Java中不可变对象是设计约定而非语法强制,需满足类final、字段private final、构造器初始化且不逸出、返回内部可变对象时深拷贝四个条件;String符合,StringBuilder不符合。

不可变对象(Immutable Object)在 Java 中不是语法层面的强制约束,而是一种设计约定:一旦创建,其状态(即所有字段的值)就无法被修改。这直接关系到线程安全、缓存可靠性与防御性编程——不是“用了就安全”,而是“必须满足条件才真正不可变”。
为什么 String 是典型不可变对象,但 StringBuilder 不是
String 的不可变性依赖三个关键设计:内部字符数组 value 被 private final 修饰;所有可能修改内容的方法(如 substring()、concat())都返回新对象;没有对外暴露可修改内部状态的 public 方法。而 StringBuilder 的 value 字段虽为 final,但其引用的 char[] 数组内容可被 append()、setCharAt() 等方法反复修改,因此它可变。
常见误判点:
- 仅靠
final修饰字段 ≠ 对象不可变(如final List,list 引用不可变,但内容可变)list = new ArrayList(); - 未深拷贝可变组件(如类中持有
final Date date,但外部仍可通过date.setTime()修改其状态) - 未阻止子类重写方法(若类未声明
final,子类可能添加 setter 或暴露内部可变对象)
手写不可变类的四个硬性条件
要让自定义类成为真正不可变对象,缺一不可:
立即学习“Java免费学习笔记(深入)”;
- 类本身用
final修饰,防止继承后被破坏封装 - 所有字段必须是
private final - 构造器完成全部初始化,且不暴露 this 引用(避免逸出)
- 任何返回内部可变对象(如
java.util.Date、int[]、ArrayList)的方法,必须返回副本而非原引用(例如用new Date(internalDate.getTime())或Collections.unmodifiableList()包装)
示例片段:
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
// 没有 setter,不返回可变内部状态
}
不可变对象在并发场景下的真实价值与限制
不可变对象天然线程安全,因为无共享可变状态,不需要同步、锁或 volatile——这点在高竞争读场景(如配置类、枚举常量、函数式数据结构)中能显著降低开销。但它不能解决所有并发问题:
- 不可变对象的“创建过程”仍需线程安全(如用非线程安全的 builder 构建后再赋值,可能看到半初始化状态)
- 多个不可变对象组合的逻辑操作(如 “先读 A 再读 B,要求 A 和 B 一致”)仍需额外同步,不可变本身不保证原子性
- 过度使用会增加 GC 压力(频繁创建新对象),比如用
ImmutablePoint做高频坐标更新,不如用可变对象 + 明确同步边界
真正难的不是“让字段 final”,而是识别哪些字段表面不可变、实则暗藏可变引用;也不是“要不要不可变”,而是判断当前场景是否需要它带来的安全契约——比如 DTO 传参适合不可变,而实时计算中间状态通常不适合。









