面试题:用过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是否能够解决可见性问题,以及它是如何解决的?
因而,要想在面试时从容应对,肯定要具备体系化的技术了解,防止面试时各种”不分明“、”不理解“之类的难堪!
如果本文对您有帮忙,欢送关注和点赞;如果您有任何倡议也可留言评论或私信,您的反对是我保持创作的能源。须要设计模式、源码等相干学习材料能够戳这里收费支付材料