final 与不可变:别把两者划等号
更新时间:2026-09-20。final 的内存语义和 volatile/JMM 相关;最经典的不可变类是 String。
一、final 修饰三种位置,含义不同
// 1) 变量/字段:只能赋值一次
final int x = 1;
final List<Integer> list = new ArrayList<>();
list.add(1); // ✅ 合法!
// list = new ArrayList<>(); // ❌ 不能重新指向| 修饰 | 含义 |
|---|---|
final 局部/成员变量 | 引用(或基本类型值)不可再被赋值;基本类型值恒定,引用类型只是「地址不变」 |
final 方法 | 子类不能重写(可重载、可继承调用) |
final 类 | 不能被继承(如 String、Integer) |
核心区分:final 引用 ≠ 指向的对象不可变。final List 仍可 add/clear,final Point p 仍可 p.x = 9。
二、什么才叫「不可变对象」
一个对象创建后其对外可见状态不能再改变,才是不可变。条件:
- 类用
final防止子类破坏不可变契约(或用 record/sealed); - 所有字段
private final,不提供 setter; - 不把内部可变对象的引用泄漏出去,构造和 getter 里对可变入参/返回做防御性拷贝;
- 不在构造期间让 this 逃逸。
public final class Range {
private final int from;
private final List<String> tags;
public Range(int from, List<String> tags) {
this.from = from;
this.tags = new ArrayList<>(tags); // 拷贝:外部再改原 list 不影响我
}
public List<String> tags() {
return List.copyOf(tags); // 返回不可修改副本,杜绝外部改内部
}
}JDK 16+ 的 record 天然接近不可变(字段 private final、无 setter),但如果 record 里持有 List 等可变组件,仍要自己做拷贝/List.copyOf——record 只保证引用槽位 final,不递归保证组件对象不可变。
三、String 为什么是不可变标杆
String 被声明为 final 类、内部字符数组 private 且不提供修改方法、substring/concat 都返回新串。带来的好处:
- 线程安全:多线程共享无需同步;
- hashCode 可缓存:作为 HashMap key 安全且高效;
- 常量池驻留:字面量可放心共享(见 String 常量池与 intern);
- 安全:类名、URL、数据库连接串不会被中途篡改。
四、final 的两个进阶语义
1. 安全发布(JMM 对 final 字段的保证)
正确构造的对象,其 final 字段在构造器结束前就被保证对其他线程可见初始化好的值(无需同步),不会看到「默认零值」。这就是为什么不可变对象可以安全地跨线程共享——但前提仍是构造期间 this 不逃逸、字段没有把可变状态裸奔出去。
2. 利于 JIT
final 让「方法不会被重写、类不会有子类、字段不会变」可被编译器确定,便于内联与去虚化(见 JIT 即时编译)。但这是优化收益,不是 final 的语义承诺。
五、常见误区
final让对象线程安全?—— 只对「真正不可变对象」成立;final ArrayList被多线程并发 add 照样出问题;- final 类里的方法不用写 final?—— final 类已经不可继承,方法自然无法被重写;
- 把字段设 final 就够了?—— 还得 private + 不暴露可变引用 + 防御性拷贝;
final和finally、finalize()没关系:前者是修饰符,finally 是异常块,finalize 是早已废弃的对象回收钩子。
总结
final 用在变量上只保证「引用/值不被重新赋值」,指向的可变对象内部照样能改,用在方法上禁止重写、用在类上禁止继承,所以 final 本身不等于不可变;真正不可变要满足 final 类、private final 字段、无 setter、对可变组件防御性拷贝且不泄漏引用,record 只提供浅层次保证;不可变对象(如 String)因状态不变而天然线程安全、可缓存 hashCode、能安全入常量池,JMM 还保证正确构造的 final 字段安全发布,JIT 也更易内联,但一个 final ArrayList 被并发修改仍不安全。