learn-tech/专栏/Java并发编程78讲-完/73为什么加了final却依然无法拥有“不变性”?.md
2024-10-16 00:20:59 +08:00

207 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
73 为什么加了 final 却依然无法拥有“不变性”?
本课时我们主要讲解为什么加了 final 却依然无法拥有“不变性”。
什么是不变性
要想回答上面的问题我们首先得知道什么是不变性Immutable。如果对象在被创建之后其状态就不能修改了那么它就具备“不变性”。
我们举个例子,比如下面这个 Person 类:
public class Person {
final int id = 1;
final int age = 18;
}
如果我们创建一个 person 对象,那么里面的属性会有两个,即 id 和 age并且由于它们都是被 final 修饰的,所以一旦这个 person 对象被创建好,那么它里面所有的属性,即 id 和 age 就都是不能变的。我们如果想改变其中属性的值就会报错,代码如下所示:
public class Person {
final int id = 1;
final int age = 18;
public static void main(String[] args) {
Person person = new Person();
// person.age=5;//编译错误,无法修改 final 变量的值
}
}
比如我们尝试去改变这个 person 对象,例如将 age 改成 5则会编译通不过所以像这样的 person 对象就具备不变性,也就意味着它的状态是不能改变的。
final 修饰对象时,只是引用不可变
这里有个非常重要的注意点,那就是当我们用 final 去修饰一个指向对象类型(而不是指向 8 种基本数据类型,例如 int 等)的变量时候,那么 final 起到的作用只是保证这个变量的引用不可变,而对象本身的内容依然是可以变化的。下面我们对此展开讲解。
在上一课时中我们讲过,被 final 修饰的变量意味着一旦被赋值就不能修改,也就是只能被赋值一次,如果我们尝试对已经被 final 修饰过的变量再次赋值的话,则会报编译错误。我们用下面的代码来说明:
/**
* 描述: final变量一旦被赋值就不能被修改
*/
public class FinalVarCantChange {
private final int finalVar = 0;
private final Random random = new Random();
private final int array[] = {1,2,3};
public static void main(String[] args) {
FinalVarCantChange finalVarCantChange = new FinalVarCantChange();
// finalVarCantChange.finalVar=9; //编译错误不允许修改final的变量(基本类型)
// finalVarCantChange.random=null; //编译错误不允许修改final的变量(对象)
// finalVarCantChange.array = new int[5];//编译错误不允许修改final的变量数组
}
}
我们首先在这里分别创建了一个 int 类型的变量、一个 Random 类型的变量,还有一个是数组,它们都是被 final 修饰的;然后尝试对它们进行修改,比如把 int 变量的值改成 9或者把 random 变量置为 null或者给数组重新指定一个内容这些代码都无法通过编译。
这就证明了“被 final 修饰的变量意味着一旦被赋值就不能修改”而这个规则对于基本类型的变量是没有歧义的但是对于对象类型而言final 其实只是保证这个变量的引用不可变,而对象本身依然是可以变化的。这一点同样适用于数组,因为在 Java 中数组也是对象。那我们就来举个例子,看一看以下 Java 程序的输出:
class Test {
public static void main(String args[]) {
final int arr[] = {1, 2, 3, 4, 5}; // 注意,数组 arr 是 final 的
for (int i = 0; i < arr.length; i++) {
arr[i] = arr[i]*10;
System.out.println(arr[i]);
}
}
}
首先来猜测一下假设不看下面的输出结果只看这段代码你猜它打印出什么样的结果这段代码中有个 Test 而且这个类只有一个 main 方法方法里面有一个 final 修饰的 arr 数组注意数组是对象的一种现在数组是被 final 修饰的所以它的意思是一旦被赋值之后变量的引用不能修改但是我们现在想证明的是数组对象里面的内容可以修改所以接下来我们就用 for 循环把它里面的内容都乘以 10最后打印出来结果如下
10
20
30
40
50
可以看到它打印出来的是 10 20 30 40 50而不是最开始的 1 2 3 4 5这就证明了虽然数组 arr final 修饰了它的引用不能被修改但是里面的内容依然是可以被修改的
同样对于非数组的对象而言也是如此我们来看下面的例子
class Test {
int p = 20;
public static void main(String args[]){
final Test t = new Test();
t.p = 30;
System.out.println(t.p);
}
}
这个 Test 类中有一个 int 类型的 p 属性我们在 main 函数中新建了 Test 的实例 t 之后把它用 final 修饰然后去尝试改它里面成员变量 p 的值并打印出结果程序会打印出30”。一开始 p 的值是 20但是最后修改完毕变成了 30说明这次修改是成功的
以上我们就得出了一个结论final 修饰一个指向对象的变量的时候对象本身的内容依然是可以变化的
final 和不可变的关系
这里就引申出一个问题那就是 final 和不变性究竟是什么关系
那我们就来具体对比一下 final 和不变性关键字 final 可以确保变量的引用保持不变但是不变性意味着对象一旦创建完毕就不能改变其状态它强调的是对象内容本身而不是引用所以 final 和不变性这两者是很不一样的
对于一个类的对象而言你必须要保证它创建之后所有内部状态包括它的成员变量的内部属性等永远不变才是具有不变性的这就要求所有成员变量的状态都不允许发生变化
有一种说法就认为:“要想保证对象具有不变性的最简单的办法就是把类中所有属性都声明为 final”,这条规则是不完全正确的它通常只适用于类的所有属性都是基本类型的情况比如前面的例子
public class Person {
final int id = 1;
final int age = 18;
}
Person 类里面有 final int id final int age 两个属性都是基本类型的且都加了 final所以 Person 类的对象确实是具备不变性的
但是如果一个类里面有一个 final 修饰的成员变量并且这个成员变量不是基本类型而是对象类型那么情况就不一样了有了前面基础之后我们知道对于对象类型的属性而言我们如果给它加了 final它内部的成员变量还是可以变化的因为 final 只能保证其引用不变不能保证其内容不变所以这个时候若一旦某个对象类型的内容发生了变化就意味着这整个类都不具备不变性了
所以我们就得出了这个结论不变性并不意味着简单地使用 final 修饰所有类的属性这个类的对象就具备不变性了
那就会有一个很大的疑问假设我的类里面有一个对象类型的成员变量那要怎样做才能保证整个对象是不可变的呢
我们来举个例子即一个包含对象类型的成员变量的类的对象具备不可变性的例子
代码如下
public class ImmutableDemo {
private final Set<String> lessons = new HashSet<>();
public ImmutableDemo() {
lessons.add("第01讲为何说只有 1 种实现线程的方法?");
lessons.add("第02讲如何正确停止线程为什么 volatile 标记位的停止方法是错误的?");
lessons.add("第03讲线程是如何在 6 种状态之间转换的?");
}
public boolean isLesson(String name) {
return lessons.contains(name);
}
}
在这个类中有一个 final 修饰的、且也是 private 修饰的的一个 Set 对象,叫作 lessons它是个 HashSet然后我们在构造函数中往这个 HashSet 里面加了三个值,分别是第 01、02、03 讲的题目;类中还有一个方法,即 isLesson去判断传入的参数是不是属于本课前 3 讲的标题isLesson 方法就是利用 lessons.contains 方法去判断的,如果包含就返回 true否则返回 false。这个类的内容就是这些了没有其他额外的代码了。
在这种情况下,尽管 lessons 是 Set 类型的,尽管它是一个对象,但是对于 ImmutableDemo 类的对象而言,就是具备不变性的。因为 lessons 对象是 final 且 private 的,所以引用不会变,且外部也无法访问它,而且 ImmutableDemo 类也没有任何方法可以去修改 lessons 里包含的内容,只是在构造函数中对 lessons 添加了初始值,所以 ImmutableDemo 对象一旦创建完成,也就是一旦执行完构造方法,后面就再没有任何机会可以修改 lessons 里面的数据了。而对于 ImmutableDemo 类而言,它就只有这么一个成员变量,而这个成员变量一旦构造完毕之后又不能变,所以就使得这个 ImmutableDemo 类的对象是具备不变性的,这就是一个很好的“包含对象类型的成员变量的类的对象,具备不可变性”的例子。
总结
下面进行总结,在本课时,我们首先介绍了什么是不变性,然后介绍了用 final 修饰一个对象类型的变量的时候,只能保证它的引用不变,但是对象内容自身依然是可以变的。
之后,我们探讨了关键字 final 和不变性的关系。我们知道仅仅把所有的成员变量都用 final 修饰并不能代表类的对象就是具备不变性的。