@[toc]
1. 引入
Java中被final关键字修饰的变量表示表示最终的,不可改变。常见的用法有四种:
- 修饰一个类:一个类如果被final修饰,当前类不能有子类,而且其中的所有成员方法都无法进行覆盖重写。格式:
public final class 类名称{
...
}
- 修饰一个方法,当final关键字用来修饰一个方法的时候,这个方法就是最终方法,无法被覆盖重写。格式:
修饰符 final 返回值类型 方法名称(参数列表){
...
}
对于类、方法来说,abstract关键字和final关键字不能同时使用,因为矛盾
修饰一个局部变量,当变量被final修饰,一次赋值终生不变。对于基本类型来说,不可变说的是变量当中的数据不可变;对于引用类型来说,不可变说的是变量当中的地址值不可变
修饰一个成员变量,当成员变量被final关键字修饰,变量照样不可变
- 由于成员变量具有默认值,所以使用了final之后必须手动赋值,不会再给默认值
- 对于被final修饰的成员变量,要么使用直接赋值,要么通过构造方法进行赋值,二者选其一
- 必须保证类中所有的重载的构造方法都最终会对final的成员变量进行赋值
知道了如何使用final关键字来保证方法或是变量的不可变性,那么为什么使用了final关键字就可以做到这些呢?下面我们看一下它的底层实现原理。
2. 原理
假设此时代码如下所示:
/**
* @Author dyliang
* @Date 2020/8/27 15:01
* @Version 1.0
*/
public class FinalDemo {
public final int number = 10;
}
如果此时在主方法中为number重新赋值,那么就会看到编译错误如下所示:
编译程序后,查看方法对应的字节码指令,如下所示:
0 aload_0
1 invokespecial #1 <java/lang/Object.<init>>
4 aload_0
5 bipush 10
7 putfield #2 <keyWords/FinalDemo.number>
10 return
通过上面的字节码指令可以看出,被final修饰的变量的赋值使用了putfiled指令,操作系统会在该指令后加入写屏障,保证其他线程读到它的值时不会出现为0的情况。那为什么使用了putfield指令后,就可以保证其他线程读到的一定是它初始化后的结果呢?下面,我们从内存层面继续往下看一下它的原理。
我们知道写屏障和读屏障一个重要的特性就是禁止指令间的重排序,特别是对于final域来说,编译器和处理器都需要遵从如下的两条规则:
- 任意构造函数中对一个final域的写入,与随后把这个构造对象的引用赋值给另一个引用变量,这两个操作不能重排序
言外之意:在对象引用为任意线程可见之前,对象的final域已经被正确的初始化过了。
- 初次读一个包含final域对象的引用,与之后初次读这个final域,这两个操作之间不能重排序
言外之意:只有得到了包含final域对象的引用,才能后读到final域的值。
final关键字通过上述的两条规则,保证了在写final域和读final域时的正确性。
3. 优点
final关键字提高了性能,JVM和Java应用都会缓存final变量
final变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销
使用final关键字,JVM会对方法、变量及类进行优化