final
作为Java中一个很常见的关键字,可以用于声明在数据、方法、类上。用通俗的一句话将,用final
关键字修饰的东西,那么它指向的东西就是不可变的。
final
的基础上使用
类
final
用在类上,就表示这个类是不能继续继承的了,没有子类。final
类中所有方法也都是隐式final
,也就没有必要对每个方法添加final
关键字了。
这里来了个问题,如果要拓展final
类型的类,又不能继承,那怎么办?那就不用继承,用组合来实现:
class FinalString{
private String innerString;
// ...init & other methods
// 支持老的方法
public int length() {
return innerString.length(); // 通过innerString调用老的方法
}
// 新方法
public String toMyString(){
//...
}
}
这样就是一个支持了老方法和可以自行补充新方法的类了。
方法
即表明这个方法是不能被重写的。要注意:
private
方法默认是final
final
方法可以被重载
很好理解,因为每个private
方法都不可被继承、重写,就相当于加了一个隐式的final
在里面。注意重载和重写的区别:
- 重写(
Override
)
子类实现了和父类方法声明相同的方法,基于里氏替换原则,有两个限制:
- 子类方法访问权限必须大于等于父类方法
- 子类方法返回类型必须是父类方法返回的类型或其子类型
可以用@Override
注解让编译器检查是否满足限制。
- 重载(Overload)
指一个类中,一个方法和另一个方法,名称相同,参数类型、个数、顺序至少一个不同。
参数
final
修饰的参数就表明这个参数在方法内部无法被改变。
变量
通过以下几个问题来进行展开:
所有的
final
修饰的字段都素hi编译器常量吗?
可以是编译器常量和非编译器常量。
public class Test {
//编译期常量
final int i = 1;
final static int J = 1;
final int[] a = {1,2,3,4};
//非编译期常量
Random r = new Random();
final int k = r.nextInt();
public static void main(String[] args) {
//...
}
}
demo
中k
的值由随机数对象决定,因此k
的值不是编译器常量,是实例初始化时决定的。但是k
的值被初始化以后无法被更改。
static final
static final
的则表明它占据了一段不可改变的空间,必须在定义时进行赋值。
public class FinalStaticDemo {
static Random r = new Random();
final int k1 = r.nextInt(10);
static final int k2 = r.nextInt(10);
public static void main(String[] args) {
FinalStaticDemo t1 = new FinalStaticDemo();
System.out.println("t1.k1 : " + t1.k1 + ", t1.k2 : " + t1.k2);
FinalStaticDemo t2 = new FinalStaticDemo();
System.out.println("t2.k1 : " + t2.k1 + ", t2.k2 : " + t2.k2);
}
}
输出:
t1.k1 : 9, t1.k2 : 1
t2.k1 : 0, t2.k2 : 1
因为k1
没有static
所以属于到单独的类实例,而k2
加了static
,那么它就属于这个类的,所以不会变。
blank final
空白的final
指的是声明为final
声明的时候没给值,但是该字段使用前必须被赋值,所以必须在构造函数里面进行赋值。同时一旦赋值,就不能改变。
public class Test {
final int i1 = 1; // 声明处赋值定义
final int i2; // 空白final,交由构造函数进行赋值
public Test() {
i2 = 1;
}
public Test(int x) {
this.i2 = x;
}
}
final
的重排序
下面总结下final
的重排序原理,主要是final
在多线程场景下的一些知识。
final
域为基本类型
public class FinalDemo {
private int a; //普通域
private final int b; //final域
private static FinalDemo finalDemo;
public FinalDemo() {
a = 1; // 1. 写普通域
b = 2; // 2. 写final域
}
public static void writer() {
finalDemo = new FinalDemo();
}
public static void reader() {
FinalDemo demo = finalDemo; // 3.读对象引用
int a = demo.a; //4.读普通域
int b = demo.b; //5.读final域
}
}
假设线程A、B分别执行上述demo
中的writer
、reader
方法。
final
域的写重排序规则
写final
域的重排序规则为:
JMM
禁止编译器把final
域的写重排序到构造函数之外- 编译器会在
final
域卸职后,构造函数return
之前,插入一个storestore
屏障,禁止将final
域的写重排序到构造函数之外。
对于demo
中的write
方法,实际做了两件事:
- 构造一个
FinalDemo
对象 - 将该对象赋值给成员变量
finalDemo
因此可能存在一个时序图:
final
域的写重排序规则保证了b
的写入在构造函数返回之前,所以能保证线程B正确读取到finalDemo
的final
域b。但是因为a、b之间没有数据关联,所以a的赋值可能发生在构造函数后。所以当线程B拿到的对象引用可能是一个未完全初始化成功的对象,读取的域a并未初始化。
final
域的读重排序规则
读final域重排序规则为:在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM
会禁止这两个操作的重排序。(注意,这个规则仅仅是针对处理器),处理器会在读final域操作的前面插入一个LoadLoad
屏障。实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。就是说,对象中final
域的读,会保证发生在对该对象引用的读之后。
read()
方法主要包含了三个操作:
- 初次读引用变量
finalDemo
; - 初次读引用变量
finalDemo
的普通域a; - 初次读引用变量
finalDemo
的final与b;
假设线程A写过程没有重排序,那么线程A和线程B有一种的可能执行时序为下图:
因为对普通域a
没有做该重排序的处理,所以对a的读会出现问题,还未读到对象引用就读了该对象的值。而对final
域的读就没有这个问题。
final
域为引用类型
final
对象的写重排序规则
针对引用类型的重排序约束:在构造函数内对一个final
域的对象的成员域的写入,是不能与随后在构造函数之外对这个被构造对象的引用赋予给一个引用变量进行重排序。即构造函数内对一个final
域的对象的成员域的写入必须是咸鱼这个被构造函数的对象的引用的赋值。
public class FinalReferenceDemo {
final int[] arrays;
private FinalReferenceDemo finalReferenceDemo;
public FinalReferenceDemo() {
arrays = new int[1]; //1
arrays[0] = 1; //2
}
public void writerOne() {
finalReferenceDemo = new FinalReferenceDemo(); //3
}
public void writerTwo() {
arrays[0] = 2; //4
}
public void reader() {
if (finalReferenceDemo != null) { //5
int temp = finalReferenceDemo.arrays[0]; //6
}
}
}
假设线程A执行writeOne
,线程B执行writeTwo
,线程C执行reader()
则可能出现的时序图如下:
由于对final域的写禁止重排序到构造方法外,因此1和3不能被重排序。由于一个final域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此2和3不能重排序。
final
对象的读重排序规则
JMM
可以确保线程C至少能看到写线程A对final引用的对象的成员域的写入,即能看下arrays[0] = 1,而写线程B对数组元素的写入可能看到可能看不到。JMM
不保证线程B的写入对线程C可见,线程B和线程C之间存在数据竞争,此时的结果是不可预知的。如果可见的,可使用锁或者volatile
。
关于final重排序的总结
按照final修饰的数据类型分类:
- 基本数据类型:
final域写
:禁止final域写与构造方法重排序,即禁止final域写重排序到构造方法之外,从而保证该对象对所有线程可见时,该对象的final域全部已经初始化过。final域读
:禁止初次读对象的引用与读该对象包含的final域的重排序。
- 引用数据类型:
额外增加约束
:禁止在构造函数对一个final修饰的对象的成员域的写入与随后将这个被构造的对象的引用赋值给引用变量重排序
final
对象在构造函数中的“溢出”问题
上述的重排序都是在构造函数中讨论的,但是对于构造函数外,仍可能存在问题。
public class FinalReferenceEscapeDemo {
private final int a;
private FinalReferenceEscapeDemo referenceDemo;
public FinalReferenceEscapeDemo() {
a = 1; //1
referenceDemo = this; //2
}
public void writer() {
new FinalReferenceEscapeDemo();
}
public void reader() {
if (referenceDemo != null) { //3
int temp = referenceDemo.a; //4
}
}
}
假设一个线程A执行writer
方法另一个线程执行reader
方法。因为构造函数中操作1和2之间没有数据依赖性,1和2可以重排序,先执行了2,这个时候引用对象referenceDemo
是个没有完全初始化的对象,而当线程B去读取该对象时就会出错。尽管依然满足了final域写重排序规则:在引用对象对所有线程可见时,其final域已经完全初始化成功。但是,引用对象this
逸出,该代码依然存在线程安全的问题。
一道思考题
byte b1=1;
byte b2=3;
byte b3=b1+b2;//当程序执行到这一行的时候会出错,因为b1、b2可以自动转换成int类型的变量,运算时java虚拟机对它进行了转换,结果导致把一个int赋值给byte-----出错
但是加上final
以后就没问题了
final byte b1=1;
final byte b2=3;
byte b3=b1+b2;
加了final
就相当于强制这个类型不能进行转换,直接相加。而java
虚拟机为了指令的统一快捷,对于short
、byte
这些变量的相加,是先转换为int
再进行操作。而加了final
实际就将这个过程强制无法进行。