问题
1. 所有的 final 修饰的字段都是编译期常量吗?
2. 如何理解 private 所修饰的方法是隐式的 final?
3. 说说 final 类型的类如何拓展?
4. 比如 String 是 final 类型,我们想写个 MyString 复用所有 String 中方法,同时增加一个新的 toMyString() 的方法,应该如何做?
5. final 方法可以被重载吗?
6. 父类的 final 方法能不能够被子类重写? 不可以
7. 说说 final 域重排序规则?
8. 说说 final 的原理?
9. 使用 final 的限制条件和局限性?
当声明一个 final 成员时,必须在构造函数退出前设置它的值。将指向对象的成员声明为 final 只能将该引用设为不可变的,而非所指的对象。
10. 为什么 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
方法,线程 B 执行 reader
方法。
基础使用
修改类
当某个类的整体定义为 final 时,就表明了你不能打算继承该类,而且也不允许别人这么做。即这个类是不能有子类的。在构造函数中,1 和 2 没有关联关系,因此可能存在重排序:先执行 2,此时 referenceDemo
是一个还未完全初始化的对象。而当线程 B 读取该对象时就会出错。
修饰方法
- private 方法是隐式的。
- final final 方法是可以被重载的。
-
修饰变量
所有的 final 修饰的变量都是编译期常量么? 答案并不是。如果 k 的值无法在编译期被确认,那么需要等到运行时再确认,但一旦被赋值,就无法被修改。
static final
只占据一段不能改变的存储空间,它不能在定义的时候进行赋值,否则编译器不予通过。blank 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域 } }
写 final 域重排序规则
JMM禁止编译器把final域的写重排序到构造函数之外;
- 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。
写 final 域的重排序规则可以确保:在对象引用被任意线程可见之前,对象的 final 域已被正确初始化过了,而普通域不具备这个保障。比如步骤 3
,线程 B 有可能得到一个未正确初始化的对象。
读 final 域重排序规则
读 final 域重排序规则为:在一个线程中,初次读对象引用和初次读该对象包含的 final 域,JMM 会禁止这两个操作的重排序。
读对象的普通域被重排序到了读对象引用的前面就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。而final域的读操作就“限定”了在读final域变量前已经读到了该对象的引用,从而就可以避免这种情况。
读 final 域的重排序规则可以确保:在读一个对象的 final 域之前,一定会先读这个包含这个 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 执行
writerOne()
方法,执行完后线程 B 执行writeTwo()
方法,线程 C 执行reader
方法。执行的可能时序图如下:
分析:
- final 构造器原则:对于 final 域的写操作禁止重排序到构造方法外。因此 1 和 3 不能被重排序。
- 由于一个 final 域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋值给引起引用变量重排序,因此 2 和 3 不能重排序。即对象写入与对象赋值不能重排序。
读操作
JMM 可以确保线程 C 至少能看到写线程 A 对 final 引用的对象的成员域的写入,即能看下 arrays[0] = 1,而写线程 B 对数组元素的写入可能看到可能看不到。JMM 不保证线程 B 的写入对线程 C 可见,线程 B 和线程 C 之间存在数据竞争,此时的结果是不可预知的。如果可见的,可使用锁或者 volatile。
final 重排序总结
- 基本数据类型:
- final 域写:禁止 final 域写与构造方法重排序,即禁止 final 域写重排序到构造方法之外,从而保证该对象对所有线程可见时,该对象的 final 域全部已经初始化过。
- final 域读:禁止初次读对象的引用与读该对象包含的 final 域的重排序。
- 引用数据类型:
- 额外增加约束:禁止在构造函数对一个 final 修饰的对象的成员域的写入与随后将这个被构造的对象的引用赋值给引用变量重排序 。
即 final 与构造方法紧密相关,从而与类的初始化紧密相关。如果类的初始化未完成,提早暴露可能引发程序 BUG。