不可变对象需同时满足:类为final、所有字段private final、无修改内部状态的方法;final仅保证引用不变,不阻止对象状态改变;适合值对象、配置类和HashMap键,但高频率修改场景需权衡GC开销。
不可变对象必须满足的三个条件
一个 Java 对象要被认定为不可变,不是靠加个 final 关键字就完事的。它得同时满足:类本身用 final 修饰(防止被继承)、所有字段都是 private final、并且没有暴露可修改内部状态的方法(比如不提供 setter,也不返回可变对象的引用)。漏掉任意一条,都可能被绕过防护。
-
String是典型不可变类:它的value字段是private,且所有 public 方法(如
final char[]
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 就是典型的非线程安全可变类。
- 多个线程共用同一个
SimpleDateFormat实例调用parse(),可能抛出java.lang.NumberFormatException或返回错误日期 - 解决方式不是加
synchronized(性能差),而是改用DateTimeFormatter(不可变、线程安全),或每次新建SimpleDateFormat实例 - 使用
ThreadLocal也可行,但要注意内存泄漏风险(尤其在线程池中)
什么时候该设计成不可变对象
不是所有对象都适合不可变。它最适合那些状态一旦创建就不应改变、且会被多处共享的场景。
- 值对象(value object):如
LocalDate、BigInteger、自定义的Money、Range - 配置类:如果整个应用生命周期内配置只读,用不可变对象能避免意外篡改
- 作为
HashMap的 key:不可变保证了hashCode()值稳定,不会因字段变化导致 key “消失” - 但频繁修改的业务实体(如
User订单状态流转)强行不可变会导致大量临时对象,GC 压力上升
不可变性的代价是每次“修改”都要新建对象,所以关键看修改频次和对象大小。小而稳的数据结构值得不可变;大而频繁变更的状态,得权衡清楚。









