共计 6771 个字符,预计需要花费 17 分钟才能阅读完成。
面试题:用过 final 关键字吗?它有什么作用
面试考察点
考查目标:理解面试者对 Java 基础知识的了解
考查人群:工作 1 - 5 年,工作年限越高,对于基础知识了解的深度就越高。
背景常识
final
关键字大家都不生疏,然而要达到深度了解,还是欠缺了一些。咱们从三个方面去了解 final
关键字。
final
关键字的根本用法- 深度了解
final
关键字 final
关键字的内存屏障语义
final 的根本用法
final
关键字,在 Java 中能够润饰类、办法、变量。
- 被 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 进行继承
- 被 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 - 被 final 润饰的成员变量是用得最多的中央。
-
- 对于一个 final 变量,如果是根本数据类型的变量,则其数值一旦在初始化之后便不能更改;final 润饰的变量能间接实现常量的性能,而常量是全局的、不可变的,因而咱们同时应用 static 和 final 来润饰变量,就能达到定义常量的成果。
- 如果是援用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
被 final 润饰的变量的初始化
- 在定义时初始化属性的值
public class TCCClass {private final String name; public static void main(String[] args) {}}
上述代码在运行时会提醒如下谬误
java: 变量 name 未在默认结构器中初始化
批改成上面的形式即可。
public class TCCClass {private final String name=”name”;} - 在构造方法中赋值
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
以参数的模式传进来,对匿名外部类中的拷贝(变量 a
和b
的拷贝)进行赋值初始化。
也就是说,在 run
办法中拜访的变量 a
和b
,是局部变量 a
和b
的一个正本,为什么这么设计?
在
test
办法中,有可能test
办法执行完结且a
和b
的申明周期也完结了,然而 Thread 这个匿名外部类可能还未执行完,那么在 Thread 中的run
办法中持续应用局部变量a
和b
就会有问题。然而又要实现这样的成果,怎么办呢?所以 Java 采纳了复制的伎俩来解决这个问题。
然而这样一来,还是存在一个问题,就是 test
办法中的成员变量与匿名外部类 Thread 中的成员变量的正本呈现 数据不统一 怎么办?
这样就达不到本来的用意和要求。为了解决这个问题,java 编译器就限定必须将变量 a
和b
限度为 final 变量,不容许对变量 a
和b
进行更改(对于援用类型的变量,是不容许指向新的对象),这样数据不一致性的问题就得以解决了。
另外,如果咱们这么写也是容许的,jvm 会隐式给 a
和b
减少 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 变量的写与读。
- 写的重排序规定能够保障,在对象援用对任意线程可见之前,对象的 final 变量曾经正确初始化了,而一般变量则不具备这个保障;
- 读的重排序规定能够保障,在读一个对象的 final 变量之前,肯定会先读这个对象的援用。如果读取到的援用不为空,依据下面的写规定,阐明对象的 final 变量肯定以及初始化结束,从而能够读到正确的变量值。
如果 final 变量的类型是援用型,那么构造函数内,对一个 final 援用的对象的成员域的写入,与随后在构造函数外把这个被结构对象的援用赋值给一个援用变量,这两个操作之间不能重排序。实际上这也是为了保障 final 变量在对其余线程可见之前,可能正确的初始化实现。
对于指令重排序相干的内容,就不在本篇文章中做开展,在后续的面试题中,会做具体的剖析。
final 关键字的益处
上面为应用 final 关键字的一些益处:
- final 关键字进步了性能,JVM 和 Java 利用都会缓存 final 变量(理论就是常量池)
- final 变量能够平安的在多线程环境下进行共享,而不须要额定的同步开销
问题解答
面试题:用过 final 关键字吗?它有什么作用
答复:final 关键字示意不可变,它能够润饰在类、办法、成员变量中。
- 如果润饰在类上,则示意该类不容许被继承
- 润饰在办法上,示意该办法无奈被重写
- 润饰在变量上,示意该变量无奈被批改,而且 JVM 会隐性定义为一个常量。
另外,final
润饰的关键字,还能够防止因为指令重排序带来的可见性问题,起因是,final 遵循两个重排序规定
- 构造函数内,对一个 final 变量的写入,与随后把这个被结构对象的援用赋值给一个变量,这两个操作之间不可重排序。
- 首次读一个蕴含 final 变量的对象,与随后首次读这个 final 变量,这两个操作之间不能够重排序。
问题总结
恰好是平时常常应用的一些工具或者技术,所波及到的知识点越多。
就这个问题来说,在面试时的考察点太多了,比方:
- 如何毁坏 final 规定
- 带 static 和 final 润饰的属性,能够被批改吗?
- final 是否能够解决可见性问题,以及它是如何解决的?
因而,要想在面试时从容应对,肯定要具备体系化的技术了解,防止面试时各种”不分明“、”不理解“之类的难堪!
如果本文对您有帮忙,欢送关注和点赞;如果您有任何倡议也可留言评论或私信,您的反对是我保持创作的能源。须要设计模式、源码等相干学习材料能够戳这里收费支付材料