不可变对象需同时满足:类为final、所有字段private final、无修改内部状态的方法;final仅保证引用不变,不阻止对象状态改变;适合值对象、配置类和HashMap键,但高频率修改场景需权衡GC开销。

不可变对象必须满足的三个条件
一个 Java 对象要被认定为不可变,不是靠加个 final 关键字就完事的。它得同时满足:类本身用 final 修饰(防止被继承)、所有字段都是 private final、并且没有暴露可修改内部状态的方法(比如不提供 setter,也不返回可变对象的引用)。漏掉任意一条,都可能被绕过防护。
-
String是典型不可变类:它的value字段是private final char[],且所有 public 方法(如substring())都返回新对象,不修改原数组 - 如果字段是
private final List,但通过 getter 返回了原始ArrayList引用,外部就能调用add()修改内容——这不算真正不可变 - 构造器里若直接赋值传入的可变对象(如
this.data = data;),必须做防御性拷贝:this.data = new ArrayList(data);
为什么 final 修饰引用不等于对象不可变
final 只保证该变量不能再指向别的对象,但不阻止对象自身状态被修改。这是最常被误解的一点。
public class Person {
private final List hobbies;
public Person(List h) {
this.hobbies = h; // ❌ 危险:hobbies 指向外部传入的 ArrayList
}
public List getHobbies() {
return hobbies; // ✅ 外部拿到后可直接 add/remove
}
}
上面代码中,hobbies 是 final,但 ArrayList 本身是可变的。正确做法是用 Collections.unmodifiableList() 包装,或在构造时深拷贝。
可变对象在并发场景下的典型问题
多线程共享可变对象时,如果没有同步机制,会出现可见性、原子性、重排序三类问题。比如 SimpleDateFormat 就是典型的非线程安全可变类。
云点滴客户解决方案是针对中小企业量身制定的具有简单易用、功能强大、永久免费使用、终身升级维护的智能化客户解决方案。依托功能强大、安全稳定的阿里云平 台,性价比高、扩展性好、安全性高、稳定性好。高内聚低耦合的模块化设计,使得每个模块最大限度的满足需求,相关模块的组合能满足用户的一系列要求。简单 易用的云备份使得用户随时随地简单、安全、可靠的备份客户信息。功能强大的报表统计使得用户大数据分析变的简单,
立即学习“Java免费学习笔记(深入)”;
- 多个线程共用同一个
SimpleDateFormat实例调用parse(),可能抛出java.lang.NumberFormatException或返回错误日期 - 解决方式不是加
synchronized(性能差),而是改用DateTimeFormatter(不可变、线程安全),或每次新建SimpleDateFormat实例 - 使用
ThreadLocal也可行,但要注意内存泄漏风险(尤其在线程池中)
什么时候该设计成不可变对象
不是所有对象都适合不可变。它最适合那些状态一旦创建就不应改变、且会被多处共享的场景。
- 值对象(value object):如
LocalDate、BigInteger、自定义的Money、Range - 配置类:如果整个应用生命周期内配置只读,用不可变对象能避免意外篡改
- 作为
HashMap的 key:不可变保证了hashCode()值稳定,不会因字段变化导致 key “消失” - 但频繁修改的业务实体(如
User订单状态流转)强行不可变会导致大量临时对象,GC 压力上升
不可变性的代价是每次“修改”都要新建对象,所以关键看修改频次和对象大小。小而稳的数据结构值得不可变;大而频繁变更的状态,得权衡清楚。









