一、什么是不可变性(Immutable)
- 如果对象在
被创建后,状态就不能被修改
,那么它就是不可变的 - 这个对象不能被修改指:
- 对象指向(引用)不可变
- 字段不可变
- 成员变量不可变
案列演示: person对象,age和name属性都不能再变
/**
* 不可变的对象,演示其他类无法修改这个对象
* public也不行
*/
public class Person {
final String name = "李白";
final int age = 18;
}
测试修改被final修饰的类属性,发现编译都过不了
如果这个类,有一个可修改的成员变量,他不被final修饰,那么这个类就不具备不可变性,可以修改:
具有不可变性的对象,一定线程安全!!!我们不需要采取任何额外的安全措施,也可以保证线程安全
二、final的作用
1、早期
早起的Java版本中,会将final修饰的方法转为内嵌调用
内嵌调用:一个方法调用另外一个final方法,那么就把这个final方法里面的东西全部都挪过来。相当于只在同一个方法里调用完成整个工作。不是方法之间调来调去,减少性能损耗。
2、现在
- 修饰类:防止被继承
- 修饰方法:防止被重写
- 修饰变量:防止被修改
final 天生 线程安全 ,不需要额外的同步开销。
三、final的3种用法
1、修饰变量
(1)含义
被final修饰的变量,意味着值不能被修改
。如果修饰的是对象
,那么对象的引用不能变
,但是引用的那个对象自身的属性依然可以变化
。比如下面这样改变对象的引用是不可以的:
final Person p = new Person();
p = new Person(); // 重新赋值一个别的对象是不允许的
但是修改对象的属性是可以的,如下:
final Person p = new Person();
p.score = 100; // 对象的属性是可以修改的
(2)3种变量的赋值时机
① final instance variable(类中的final属性)
属性
被声明为final后,改变量则只能被赋值一次
。且一旦被赋值,final的变量就不能在改变
,无论如何也不会变。
类中的final属性允许在下面三种时机进行赋值:
1、声明变量时在等号右边
直接赋值
public class FinalVariableDemo {
private final int a = 10;
}
2、构造函数
中赋值
public class FinalVariableDemo {
private final int a;
public FinalVariableDemo(int a) {
this.a = a;
}
}
3、类的初始化代码块中
赋值(不常用)
public class FinalVariableDemo {
private final int a;
{
a = 4; // 初始化代码块中赋值
}
}
对于类中的final属性,必须在上面三种赋值方式中选择一种,否则就会提示错误:
② final static variable(类中的static final属性)
类中的static final属性的赋值时机有两种,如下:
除了在声明变量的等号右边直接赋值
外,static final变量还可以用static初始代码块赋值
,但是不能用普通的初始化代码块赋值
1、声明变量时在等号右边
直接赋值
public class FinalVariableDemo {
private static final int a = 1;
}
2、static初始代码块赋值
public class FinalVariableDemo {
private static final int a;
static {
a = 1;
}
}
③ final local variable(方法中的final变量)
方法中的final变量的赋值时机和前面两种不同,由于这里的变量是在方法里的;所以没有构造函数,也不存在初始代码块。它不规定赋值时机,只要求在使用前必须赋值
,这一点和方法中的其他非final变量的要求也是一样的。
(3)为什么要规定赋值时机?
如果在final变量在初始化时不赋值,而像普通变量一样自动会有一个默认值,那么以后再想赋值时,就意味着该变量从默认值
变成你的赋值
,这就意味着final变量的值修改了,违反了final不变性的原则了
2、修饰方法
(1)构造方法不允许final修饰
(2)不可被重写,也就是不能被override
即便是子类有同样名字的方法,那也不是重写的父类方法,这个和static不能被重写
是一个道理
3、修饰类
修饰类时,表示类不可被继承
-
String就是被final修饰
四、final的注意点
- final修饰对象的时候,只是对象的引用不可变,而对象本身的属性是可以变化的
- 可以养成一个良好的编程习惯:对于要求不能变的变量或者属性,尽量使用final修饰,同时也能向其他开发者暗示该变量不能变。
五、不变性和final的关系
不变形并不
意味着,简单地用final修饰就是不可变
- 对于
基本数据类型
,确实被final修饰后就具备不可变性 - 但是对于
对象类型
,需要该对象保证自身被创建后,状态永远不会变才可以- 满足以下条件时,对象类型才满足不可变性
- 对象创建后,其状态就不能修改
- 所有属性都是final修饰的
- 对象创建过程中没有发生逸出
- 满足以下条件时,对象类型才满足不可变性
如下所示,该类的对象创建之后,对象整体都是不可变的:
/**
* 描述: 一个属性students是对象,但是整体不可变,其他类无法修改set里面的数据
* 只在初始化过程中赋值,只提供读取的方法isStudent(String name)给外部,其他类没有修改的机会
*/
public class ImmutableDemo {
private final Set<String> students = new HashSet<>();
public ImmutableDemo() {
students.add("李白");
students.add("杜甫");
students.add("白居易");
}
public boolean isStudent(String name) {
return students.contains(name);
}
}
六、栈封闭技术
在方法里新建的局部变量,实际上是存储在每个线程私有的栈空间,而每个栈空间是不能被其他线程所访问到的,所以不会有线程安全问题。这就是著名的“栈封闭”技术,是“线程封闭”技术的一种情况。
换句话说,方法内的变量,多个线程之间不共享,不会被其他线程所访问到
/**
* 演示栈封闭的两种情况,基本变量 和 对象
* 先演示线程争抢带来错误结果,然后把变量放到方法内,情况就变了
*/
public class StackConfinement implements Runnable {
//类共享变量,被两个线程共享
int index = 0;
public void inThread(){
//方法内局部变量,多个线程之间不共享
int neverGoOut = 0;
for (int i = 0; i < 10000; i++) {
neverGoOut++;
}
System.out.println("栈内保护的数字是线程安全的: "+neverGoOut);
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
index++;
}
inThread();
}
public static void main(String[] args) throws InterruptedException {
StackConfinement r = new StackConfinement();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(r.index);
}
}