不可变设计
不可变设计
如果一个对象是不可变的,属性无法被修改,那么这个对象即使被共享,也不会发生线程安全问题
不可变类的使用
如果一个类的属性是可变的,那么在多个线程并发访问时,会出现并发安全问题
如果需要并发访问,需要使用不可变类
不可变类的设计
String是典型的不可变类,通过final关键字修饰属性和类来保证不可变性,或者也可以通过私有属性+不设置set方法来实现
这句话的核心在于:继承(Inheritance)是破坏不可变性(Immutability)的最大潜伏者。
“防止子类无意间破坏不可变性”这句话,通常是在讲 不可变对象 (Immutable Object) 的设计原则(比如 Java 的 String 类)。
我来举个具体的代码例子,你就明白为什么 “允许继承” 会是一件危险的事情。
假设场景:String 允许被继承
假设 Java 的 String 类没有加 final 关键字,那么我就可以创建一个“邪恶”的子类:
// 假设 String 没有 final,我就可以继承它
public class EvilString extends String {
private String innerValue;
public EvilString(String original) {
this.innerValue = original;
}
// 我重写了 length 方法,让它每次返回不一样的结果!
@Override
public int length() {
return (int) (System.currentTimeMillis() % 100);
}
// 我甚至可以重写 hashCode,破坏 HashMap 的键
@Override
public int hashCode() {
return 0; // 所有 EvilString 的 hash 都是 0
}
}危害在哪里?
假设你写了一个安全组件,用来验证用户密码长度:
public class SecurityCheck {
// 你期望这里传入的是一个老实本分的 String
public boolean checkLength(String password) {
// String 是不可变的,所以你觉得它的长度肯定也就是固定的
int len1 = password.length();
int len2 = password.length();
// 逻辑崩塌:对于 EvilString,两次调用 length() 可能返回不同结果
// 你的代码逻辑完全被子类给“破坏”了
return len1 == len2;
}
}
// 攻击者传入 EvilString
checkLength(new EvilString("123456"));破坏了什么?
行为一致性被破坏: 父类承诺这个对象一旦创建就不会变(不可变性),但子类通过重写方法(Override),可以在你不知情的情况下动态改变行为或返回值。
安全性被破坏: 很多安全机制(如类加载器、文件路径检查)都依赖
String的不可变性。如果传入的是一个子类,它表面上看着是String,背地里可能篡改了数据。线程安全被破坏: 不可变对象天生是线程安全的(因为不能改)。但如果子类引入了可变的成员变量(比如加了一个
private int count并且提供了setCount),那么这个子类对象就不再线程安全了。
总结
之所以说“防止子类无意间破坏”,是因为: 一旦允许继承,父类就失去了对行为的绝对控制权。
通过把类声明为 final(如 public final class String):
- 断子绝孙:彻底禁止任何人继承它。
- 绝对控制:你拿到的
String对象,它的所有行为(length(),hashCode())一定就是官方定义的那个逻辑,不可能被任何第三方代码篡改。这就构成了不可变性的最后一道防线。
无状态类的设计
final修饰字段
只能保证栈中的值不变,而不能保证堆中的值不变
这句话是很多 Java 初学者的痛点。要彻底理解它,必须深入到 Java 内存模型 (JMM) 中的 栈 (Stack) 和 堆 (Heap) 的关系。
这句话的完整含义是: final 锁住的是变量在“栈”里的那个值(引用地址),锁不住那个地址指向的“堆”里的对象(实际内容)。
1. 内存视角:引用 vs 对象
在 Java 中,当你声明一个对象变量时:
final StringBuilder sb = new StringBuilder("Hello");这行代码在内存里干了两件事:
- 在堆 (Heap):开辟了一块内存空间,存放了一个
StringBuilder对象,里面的字符数组存着['H','e','l','l','o']。假设它的内存地址是0x1234。 - 在栈 (Stack):创建了一个变量
sb。它的值是0x1234(即堆中那个对象的地址)。
2. final 到底锁住了什么?
final 就像一把锁,它只锁在了“栈”上的变量 sb 身上。
锁住引用 (地址):
sb这个变量就像一个写着地址的小纸条。final规定:这张纸条上写死的地址0x1234永远不能被擦掉重写。// ❌ 编译报错! // 试图把纸条上的地址改成 0x5678 (另一个新对象) sb = new StringBuilder("World");锁不住内容 (堆内存):
final根本管不到地址0x1234指向的那座房子里发生了什么。它只管纸条上的地址不准变,不管房子里是不是被装修了。// ✅ 合法操作! // 我顺着地址 0x1234 找到那个对象,修改了它内部的数组内容 sb.append(" World");
3. 为什么 String 是个特例?
你可能会问:“那为什么 final String s = "abc" 内容也不能变呢?”
这不是因为 final,而是因为 String 类本身的设计。 String 内部维护的 char[] value 也是 final 的,而且 String 类没有提供任何 set 方法或 append 方法来修改这个数组。 是类的设计决定了内容不可变,而不是变量上的 final 修饰符。
还有保护性拷贝,在使用substring()方法时,使用new关键字创建新的对象
4. 结合 JMM (Java 内存模型) 的可见性
深入一点讲,final 在 JMM 中还有一个重要的语义:初始化安全性 (Initialization Safety)。
普通变量: 在构造函数里给普通变量赋值,可能发生指令重排。导致另一个线程拿到了对象引用,但发现里面的变量还是 null(未完全初始化)。
final 变量: JMM 保证:只要对象构造函数执行完毕(且构造过程中没有把
this逸出),那么其他线程看到这个对象时,它里面的final字段一定是初始化好的。
总结图解
栈 (Stack) 堆 (Heap)
+-------------------+ +---------------------------+
| 变量 sb (final) | | StringBuilder 对象 |
| [ 地址: 0x1234 ]--|------->| [ char[]: "Hello World" ] |
+-------------------+ +---------------------------+
^ ^
| |
final 锁在这里 这里随便改!
(不能改成指向 0x5678) (只要对象本身提供修改方法)一句话总结:final 保证你永远只能找到这栋房子,但不能保证房子里的家具不被搬走。