面试题:用过final关键字吗?它有什么作用

面试考察点

考查目标:理解面试者对Java基础知识的了解

考查人群:工作1-5年,工作年限越高,对于基础知识了解的深度就越高。

背景常识

final关键字大家都不生疏,然而要达到深度了解,还是欠缺了一些。咱们从三个方面去了解final关键字。

  1. final关键字的根本用法
  2. 深度了解final关键字
  3. final关键字的内存屏障语义

final的根本用法

final关键字,在Java中能够润饰类、办法、变量。

  1. 被final润饰的类,示意这个类不可被继承,final类中的成员变量能够依据须要设为final,并且final润饰的类中的所有成员办法都被隐式指定为final办法.
    在应用final润饰类的时候,要留神审慎抉择,除非这个类真的在当前不会用来继承或者出于平安的思考,尽量不要将类设计为final类。
    \```java public final class TClass {
    public final String test(){ return "true";}
    } public class TCCClass extends TClass{
   public static void main(String[] args) {   }

}

   上述程序运行失去如下谬误:   ```txt   java: 无奈从最终org.example.cl03.TClass进行继承
  1. 被final润饰的办法,示意该办法无奈被重写.其中private办法会被隐式的指定为final办法。
    class SuperClass{ protected final String getName() { return “supper class”; } @Override public String toString() { return getName(); }}classSubClass extends SuperClass{ protected String getName() { return “sub class”; }}
    上述代码运行会失去如下谬误:
    java: org.example.cl03.TCCClass中的test()无奈笼罩org.example.cl03.TClass中的test() 被笼罩的办法为final
  2. 被final润饰的成员变量是用得最多的中央。
    1. 对于一个final变量,如果是根本数据类型的变量,则其数值一旦在初始化之后便不能更改;final润饰的变量能间接实现常量的性能,而常量是全局的、不可变的,因而咱们同时应用static和final来润饰变量,就能达到定义常量的成果。
    2. 如果是援用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

被final润饰的变量的初始化

  1. 在定义时初始化属性的值
    public class TCCClass { private final String name; public static void main(String[] args) { }}
    上述代码在运行时会提醒如下谬误
    java: 变量 name 未在默认结构器中初始化
    批改成上面的形式即可。
    public class TCCClass { private final String name="name";}
  2. 在构造方法中赋值
    public class TCCClass { private final String name; public TCCClass(String name){ this.name=name; }}

可能在构造方法中赋值的起因是:对于一个一般成员属性赋值时,必须要先通过构造方法实例化该对象。因而作为该属性惟一的拜访入口,JVM容许在构造方法中给final润饰的属性赋值。这个过程并没有违反final的准则。当然如果被润饰final关键字的属性曾经初始化了值,是无奈再应用构造方法从新赋值的。

反射毁坏final规定

基于上述final关键字的根本应用形容,能够晓得final润饰的属性是不可变的。

然而,通过反射机制,能够毁坏final的规定,代码如下

public class TCCClass {    private final String name="name";    public static void main(String[] args) throws Exception {        TCCClass tcc=new TCCClass();        System.out.println(tcc.name);        Field name=tcc.getClass().getDeclaredField("name");        name.setAccessible(true);        name.set(tcc,"mic");        System.out.println(name.get(tcc));    }}

打印后果如下:

namemic
知识点扩大
上述代码实践上来说应该是上面这种写法,因为通过反射批改tcc实例对象中的name属性后,应该通过实例对象间接打印出name的后果。
public static void main(String[] args) throws Exception { TCCClass tcc=new TCCClass(); System.out.println(tcc.name); Field name=tcc.getClass().getDeclaredField("name"); name.setAccessible(true); name.set(tcc,"mic"); System.out.println(tcc.name); //here}
然而理论输入后果后,发现tcc.name打印的后果没有变动?
起因是:JVM在编译期间做的深度优化机制, 就把final类型的String进行了优化, 在编译期间就会把String解决成常量,导致打印后果不会发生变化。
为了防止这种深度优化带来的影响,咱们还能够把上述代码批改成上面这种模式
public class TCCClass { private final String name=(null == null ? "name" : ""); public static void main(String[] args) throws Exception { TCCClass tcc=new TCCClass(); System.out.println(tcc.name); Field name=tcc.getClass().getDeclaredField("name"); name.setAccessible(true); name.set(tcc,"mic"); System.out.println(tcc.name); }}
打印后果如下:
namemic

反射无奈批改被final和static同时润饰的变量

把下面的代码批改如下。

public class TCCClass {    private static final String name=(null == null ? "name" : "");    public static void main(String[] args) throws Exception {        TCCClass tcc=new TCCClass();        System.out.println(tcc.name);        Field name=tcc.getClass().getDeclaredField("name");        name.setAccessible(true);        name.set(tcc,"mic");        System.out.println(tcc.name);    }}

执行后果,执行之后会报出如下异样, 因为反射无奈批改同时被static final润饰的变量:

Exception in thread "main" java.lang.IllegalAccessException: Can not set static final java.lang.String field org.example.cl03.TCCClass.name to java.lang.String    at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76)    at sun.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80)    at sun.reflect.UnsafeQualifiedStaticObjectFieldAccessorImpl.set(UnsafeQualifiedStaticObjectFieldAccessorImpl.java:77)    at java.lang.reflect.Field.set(Field.java:764)    at org.example.cl03.TCCClass.main(TCCClass.java:13)
那么被final和static同时润饰的属性,是否被批改呢?答案是能够的!

批改代码如下:

public class TCCClass {    private static final String name=(null == null ? "name" : "");    public static void main(String[] args) throws Exception {        TCCClass tcc=new TCCClass();        System.out.println(tcc.name);        Field name=tcc.getClass().getDeclaredField("name");        name.setAccessible(true);        Field modifiers = name.getClass().getDeclaredField("modifiers");        modifiers.setAccessible(true);        modifiers.setInt(name, name.getModifiers() & ~Modifier.FINAL);        name.set(tcc,"mic");        modifiers.setInt(name, name.getModifiers() & ~Modifier.FINAL);        System.out.println(tcc.name);    }}

具体思路是,把被润饰了final关键字的name属性,通过反射的形式去掉final关键字,代码实现

Field modifiers = name.getClass().getDeclaredField("modifiers");modifiers.setAccessible(true);modifiers.setInt(name, name.getModifiers() & ~Modifier.FINAL);

接着通过反射批改name属性,批改胜利后,再应用上面代码把final关键字加回来

modifiers.setInt(name, name.getModifiers() & ~Modifier.FINAL);

为什么部分外部类和匿名外部类只能拜访final变量

在理解这个问题之前,咱们先来看上面这段代码

    public static void main(String[] args)  {    }    public void test(final int b) {        final int a = 10;        new Thread(){            public void run() {                System.out.println(a);                System.out.println(b);            };        }.start();    }}

这段代码被编译后,会生成两个文件: FinalExample.class和FinalExample$1.class(匿名外部类)

通过反编译来看一下FinalExample$1.class这个类

class FinalExample$1 extends Thread {    FinalExample$1(FinalExample this$0, int var2, int var3) {        this.this$0 = this$0;        this.val$a = var2;        this.val$b = var3;    }    public void run() {        System.out.println(this.val$a);        System.out.println(this.val$b);    }}

咱们看到匿名外部类FinalExample$1的结构器含有三个参数,一个是指向外部类对象的援用,另外两个是int型变量,很显然,这里是将变量test办法中的形参b,以及常量a以参数的模式传进来,对匿名外部类中的拷贝(变量ab的拷贝)进行赋值初始化。

也就是说,在run办法中拜访的变量ab,是局部变量ab的一个正本,为什么这么设计?

test办法中,有可能test办法执行完结且ab的申明周期也完结了,然而Thread这个匿名外部类可能还未执行完,那么在Thread中的run办法中持续应用局部变量ab就会有问题。然而又要实现这样的成果,怎么办呢?所以Java采纳了复制的伎俩来解决这个问题。

然而这样一来,还是存在一个问题,就是test办法中的成员变量与匿名外部类Thread中的成员变量的正本呈现数据不统一怎么办?

这样就达不到本来的用意和要求。为了解决这个问题,java编译器就限定必须将变量ab限度为final变量,不容许对变量ab进行更改(对于援用类型的变量,是不容许指向新的对象),这样数据不一致性的问题就得以解决了。

另外,如果咱们这么写也是容许的,jvm会隐式给ab减少final关键字。

public void test(int b) {  int a = 10;  new Thread(){    public void run() {      System.out.println(a);      System.out.println(b);    };  }.start();}

final避免指令重排

final关键字,还能避免指令重排序带来的可见性问题;

对于final变量,编译器和处理器都要恪守两个重排序规定:

  • 构造函数内,对一个 final 变量的写入,与随后把这个被结构对象的援用赋值给一个变量,这两个操作之间不可重排序。
  • 首次读一个蕴含 final 变量的对象,与随后首次读这个 final 变量,这两个操作之间不能够重排序。

实际上这两个规定也正是针对 final 变量的写与读。

  1. 写的重排序规定能够保障,在对象援用对任意线程可见之前,对象的 final 变量曾经正确初始化了,而一般变量则不具备这个保障;
  2. 读的重排序规定能够保障,在读一个对象的 final 变量之前,肯定会先读这个对象的援用。如果读取到的援用不为空,依据下面的写规定,阐明对象的 final 变量肯定以及初始化结束,从而能够读到正确的变量值。

如果 final 变量的类型是援用型,那么构造函数内,对一个 final 援用的对象的成员域的写入,与随后在构造函数外把这个被结构对象的援用赋值给一个援用变量,这两个操作之间不能重排序。实际上这也是为了保障 final 变量在对其余线程可见之前,可能正确的初始化实现。

对于指令重排序相干的内容,就不在本篇文章中做开展,在后续的面试题中,会做具体的剖析。

final 关键字的益处

上面为应用 final 关键字的一些益处:

  • final关键字进步了性能,JVM和Java利用都会缓存final变量(理论就是常量池)
  • final变量能够平安的在多线程环境下进行共享,而不须要额定的同步开销

问题解答

面试题:用过final关键字吗?它有什么作用

答复:final关键字示意不可变,它能够润饰在类、办法、成员变量中。

  1. 如果润饰在类上,则示意该类不容许被继承
  2. 润饰在办法上,示意该办法无奈被重写
  3. 润饰在变量上,示意该变量无奈被批改,而且JVM会隐性定义为一个常量。

另外,final润饰的关键字,还能够防止因为指令重排序带来的可见性问题,起因是,final遵循两个重排序规定

  1. 构造函数内,对一个 final 变量的写入,与随后把这个被结构对象的援用赋值给一个变量,这两个操作之间不可重排序。
  2. 首次读一个蕴含 final 变量的对象,与随后首次读这个 final 变量,这两个操作之间不能够重排序。

问题总结

恰好是平时常常应用的一些工具或者技术,所波及到的知识点越多。

就这个问题来说,在面试时的考察点太多了,比方:

  1. 如何毁坏final规定
  2. 带static和final润饰的属性,能够被批改吗?
  3. final是否能够解决可见性问题,以及它是如何解决的?

因而,要想在面试时从容应对,肯定要具备体系化的技术了解,防止面试时各种”不分明“、”不理解“之类的难堪!

如果本文对您有帮忙,欢送关注和点赞;如果您有任何倡议也可留言评论或私信,您的反对是我保持创作的能源。须要设计模式、源码等相干学习材料能够戳这里收费支付材料