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