1.final概述
final关键字可以用来修饰变量,方法和类,用于表示修饰的内容一旦赋值之后就不会再改变了。例如我们熟悉的String类,它就是赋值之后就没办法修改了。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
这是因为每个String对象存储的都是内存地址,如果通过其中一个String修改了内存地址中的值,那么其它对象也会跟着修改,这肯定是不允许的。
2. final的具体应用场景
2.1 变量
在Java中,变量可以分为成员变量和局部变量。
2.1.1 成员变量
public class FinalDemo1 {
final static int a = 1; // 静态final成员变量:声明变量时赋值
final static char b;
final int c = 2; // 非静态final成员变量:声明变量时赋值
final char d;
final String e;
static{
b = 'b'; // 静态final成员变量:静态代码块中赋值
}
public FinalDemo1(){
d = 'd'; // 非静态final成员变量:构造方法中赋值
}
{
e = "final"; // 非静态final成员变量:非静态代码块中赋值
}
}
对于静态的成员变量,只有两种方法赋值:
- 在声明变量时进行赋值
- 在静态代码块中进行赋值
对于非静态的成员变量,只有三种方法赋值:
- 在声明变量时进行赋值
- 在构造方法中进行赋值
- 在非静态代码块中进行赋值
2.1.2 局部变量
public void testFinal(final int a){
final int b;
b = 2;
}
对于final修饰的局部变量,只能进行一次赋值,不能进行二次赋值。
2.2 方法
2.2.1 方法重写
当父类方法使用final修饰时,子类不能重写父类方法。
2.2.2 方法重载
当父类方法使用final修饰时,子类可以对final修饰的父类方法进行重载。
2.3 类
当一个类使用final修饰时,不能被其它类继承。
3. final修饰的数据类型
在Java中,数据类型可以分为两大类:基本数据类型和引用数据类型。
对于基本数据类型,我们在上面已经介绍过了。
下面我们来看下引用数据类型:
从上图中可以看到,我们创建了一个final对象。但是在标号1处,我们仍然可以对它的成员变量进行修改。但是在标号2处,我们对这个对象重新赋值则不行。
这是因为对于引用数据类型而言,它仅仅保存的是一个引用,final只能保证这个引用变量所引用的地址不会改变,即一直引用这个对象,但这个对象的属性是可以改变的。
4. final不变类
如果我们要实现一个类和类的成员变量都不能被修改,那需要满足什么条件呢:
- 使用private和final修饰符来修饰该类的成员变量
- 提供带参的构造器用于初始化类的成员变量
- 仅为该类的成员变量提供getter方法,不提供setter方法
- 如果有必要重写Object类的hashCode()和equals()方法,应该保证用equals()判断相同的两个对象其hashcode值也相等。
5. 多线程中的final
5.1 final域重排序规则
5.1.1 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域重排序规则
先给结论
写final域的重排序规则是:禁止对final域的写重排序到构造函数之外。
实现原理是:编译器会在final域写之后,构造函数return之前,插入一个storestore屏障,用于禁止处理器把final域的写重排序到构造函数之外。
我们来分析上面的代码,假设线程A执行writer方法,线程B执行reader方法。
变量a和变量b没有依赖关系,所以可以进行重排序。由于变量b是写final域,编译器,所以不会被重排序到构造方法外。而变量a是写普通域,没有这个限制,就有可能被重排序到后面。这就可能造成reader方法中,变量a取值不正确,变量b取值正确。如下图:
读final域重排序规则
先给结论
读final域的重排序规则是:禁止读对象引用和初次读该对象包含的final域进行重排序。
实现原理是:编译器会在读final域操作的前面插上一个LoadLoad屏障。
我们来分析上面的代码,假设线程A执行writer方法,线程B执行reader方法。
在reader方法中,首先是读对象的引用赋值给demo。对于普通域的变量a,由于没有规定重排序规则,则有可能被排序到读对象的引用之前,造成程序错误。而对于普通域的变量b,由于限定了重排序规则,就不会被重排序到前面。如下图:
5.1.2 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
}
}
}
写final域重排序规则
对于引用数据类型,是在基本数据类型的基础上增加了一些约束。
先给结论
写final域的重排序规则是:在构造函数内对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作不能进行重排序。
由于在构造函数内对一个final成员域的写入不能和将这个被构造的对象的引用赋给一个成员变量不能进行重排序,所以2和3不能进行重排序。如下图所示:
读final域重排序规则
如上图,JMM可以确保线程C至少能看到写线程A对final引用的对象的成员域的写入,即能看下arrays[0] = 1,而写线程B对数组元素的写入可能看到可能看不到。JMM不保证线程B的写入对线程C可见,线程B和线程C之间存在数据竞争,此时的结果是不可预知的。如果可见的,可使用锁或者volatile。
5.2 为什么final引用不能从构造函数中溢出
我们先来看一段代码:
public class FinalDemo5 {
private final int a;
private FinalDemo5 demo;
public FinalDemo5(){
a = 1; // 1
demo = this; // 2
}
public void writer(){
new FinalDemo5();
}
public void reader(){
if(demo!=null){
System.out.println(demo.a); // 3
}
}
}
在上面介绍的基本数据类型的写final域的规则中,可以使得我们在使用一个对象的引用的时候,所有该初始化的参数都初始化过了。但是这有个前提条件:不能让这个被构造的对象被其它线程可见,也就是不能溢出。
如上面的代码,我们假设线程A执行writer方法,线程B执行reader方法。
由于标号1和标号2之间不存在依赖性,所以可能进行重排序。如果发生了重排序,那线程B可能就会读到一个没有被初始化过对象,造成线程不安全。
参考文章:你以为你真的了解final吗?