final字段不等于不可变对象,因final仅保证引用不变,若指向可变对象(如ArrayList),其内部状态仍可被修改;需确保字段类型本身不可变、深拷贝可变输入、返回不可修改视图、类声明为final,并防范序列化与Builder模式中的陷阱。

为什么 final 字段不等于不可变对象
很多人以为只要把所有字段都声明为 final,类就是不可变的。这是常见误解。关键在于:如果 final 字段指向的是可变对象(比如 ArrayList、StringBuilder 或自定义的非 final 类),外部仍可通过引用修改其内部状态。
例如:final List<string> tags = new ArrayList();</string> 允许调用 tags.add("new") —— 字段引用没变,但对象内容变了。
- 必须确保所有字段类型本身是不可变的(如
String、Integer、LocalDateTime) - 若必须使用可变类型,需在构造时深拷贝(如用
new ArrayList(input)),且 getter 返回不可修改视图(Collections.unmodifiableList()) - 禁止提供任何 setter、add、clear 等修改方法
- 类自身必须声明为
final,防止子类破坏不可变性
如何安全地实现带集合字段的不可变类
以一个常见的 Person 类为例,它包含姓名和多个邮箱地址(List<string></string>)。直接返回原始列表会暴露可变性,必须做封装。
public final class Person {
private final String name;
private final List<String> emails;
public Person(String name, List<String> emails) {
this.name = Objects.requireNonNull(name);
// 深拷贝输入列表,避免外部传入可变引用
this.emails = Collections.unmodifiableList(new ArrayList<>(Objects.requireNonNull(emails)));
}
public String getName() { return name; }
// 返回不可修改副本,调用者无法修改内部状态
public List<String> getEmails() { return emails; }
}
注意:Collections.unmodifiableList() 返回的是运行时检查的代理,不是新集合;若原始集合被其他地方修改,仍可能影响它——所以构造时必须先拷贝。
立即学习“Java免费学习笔记(深入)”;
- 不要在构造器中存储外部传入的可变集合引用
- 不要在 getter 中返回
Arrays.asList()或Arrays.copyOf()的裸数组包装结果(它们仍是可变的) - 如果字段是数组,必须用
clone()或Arrays.copyOf()并配合private+final保护
不可变对象的序列化与反序列化陷阱
Java 默认序列化会绕过构造器,通过反射直接设置字段值,这会破坏不可变性保证。例如,即使字段是 final,ObjectInputStream 仍能写入非法值。
解决方案是显式定义 readObject 方法,并在其中抛出异常或重新校验:
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// 手动校验字段是否符合不变量
if (name == null || emails == null) {
throw new InvalidObjectException("Null fields not allowed");
}
}
- 如果类实现了
Serializable,必须重写readObject或使用serialPersistentFields控制序列化字段 - 更安全的做法是避免实现
Serializable,改用 JSON(如 Jackson)+ 不可变 builder 模式序列化 - Jackson 可通过
@JsonCreator和@JsonProperty强制走构造器,比默认 Java 序列化更可控
Builder 模式如何与不可变性共存
当构造参数较多时,全参数构造器难维护。Builder 是推荐方案,但它本身必须是可变的——关键在于 Builder 实例不能泄露,且 build() 后必须清空或冻结其状态。
典型错误是让 Builder 持有对最终对象字段的引用,或未校验重复 build。
- Builder 类应为静态内部类,避免隐式持有外部类引用
- build() 方法应创建新对象,不复用已有实例
- build() 后可将 Builder 字段置为
null或设标志位,防止重复调用(非强制,但利于调试) - Builder 的 setter 应返回
this,但每个 setter 都要校验输入(如非空、范围)
真正容易被忽略的是:Builder 不是“不可变”的一部分,它是构造工具;它的责任边界只到 build() 返回那一刻。之后哪怕 Builder 被篡改,也不影响已生成的不可变实例。










