关于java:阿里面试题系列工作5年第一次这么清醒的理解final关键字

31次阅读

共计 6771 个字符,预计需要花费 17 分钟才能阅读完成。

面试题:用过 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 是否能够解决可见性问题,以及它是如何解决的?

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

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

正文完
 0