不可变类必须所有字段声明为private final并在构造函数中完成初始化,防御性拷贝可变组件,类与方法均需声明final,禁用clone和默认序列化以杜绝后门。

所有字段必须用 final 且私有
不可变性的第一道防线是编译期约束:任何字段一旦赋值就不能再改。如果漏掉 final,哪怕其他逻辑都对,类在运行时仍可能被意外修改。
常见错误现象:NullPointerException 或字段值“突然变了”,往往是因为某个字段没加 final,被子类或反射绕过封装修改。
- 所有字段声明为
private final,包括基本类型、引用类型、数组 - 构造函数里必须完成全部字段初始化,不能留空或延迟赋值
- 不要提供任何
setter方法,也不要暴露可变对象的引用(比如返回ArrayList而非unmodifiableList)
构造函数要防御性拷贝可变组件
String 的不可变性不只靠 final char[] value,还因为构造时把传入的字符数组做了拷贝——否则外部仍能通过原数组改写内容。
使用场景:字段类型是 java.util.Date、int[]、ArrayList 等典型可变类型时,直接赋值等于“交出控制权”。
立即学习“Java免费学习笔记(深入)”;
- 对传入的可变对象,在构造函数中用
new ArrayList(src)、Arrays.copyOf()、new Date(src.getTime())等方式深拷贝 - 避免使用
Collections.unmodifiableXXX()包装后直接赋值——它只防止集合结构变化,不阻止元素内部状态变更 - 如果字段是第三方可变类(如
org.joda.time.DateTime),确认它本身是否不可变;否则也得拷贝
不提供修改状态的方法,且子类不能破坏契约
String 类用 final 修饰自身,就是为了防止子类重写方法、引入可变逻辑。光封字段不够,契约必须由语言机制加固。
容易踩的坑:以为只要自己不写 setXxx() 就安全了,却忘了子类可以覆写 toString() 或添加新方法偷偷改状态。
- 类声明为
final,禁止继承 - 所有方法默认
final(显式写出来更稳妥),尤其hashCode()、equals()这类可能被子类依赖的基础方法 - 如果必须支持继承(极少见),那就不能叫“不可变类”,得降级为“有效不可变(effectively immutable)”,并文档明确说明风险
注意 clone() 和序列化带来的漏洞
Java 原生序列化和 clone() 是两个常被忽略的“后门”。String 没实现 Cloneable,也没提供 readObject() 自定义,就是为了堵死这些路径。
性能影响:防御性拷贝和自定义反序列化会略微增加构造开销,但相比运行时状态失控,这点成本值得。
- 不实现
Cloneable接口,也不提供clone()方法 - 如果实现
Serializable,必须定义private void readObject(ObjectInputStream),并在其中抛InvalidObjectException,或者重新做一次防御性拷贝 - 避免使用 Lombok 的
@Data或@Builder——它们默认生成可变构造器和 setter,与目标冲突
真正难的不是写满 final,而是识别哪些对象表面不可变、实则藏着可变字段(比如某些包装类内部持有一个 StringBuilder)。每次加一个字段,都要问一句:它会不会在别处被改?









