Java中只暴露getter的类仍可能被修改,因返回可变对象引用会导致外部直接修改内部状态;需用Collections.unmodifiableXXX或Guava ImmutableXXX包装集合,并确保字段final、构造即初始化、元素不可变。

Java 中只暴露 getter 的类为什么还是可能被修改
只写 getXXX() 方法不等于安全。如果返回的是可变对象(比如 ArrayList、HashMap、自定义的非 final 类),调用方拿到引用后直接修改内容,原始字段就“被动更新”了。常见现象是:外部调用了 getList().add(x),结果类内部状态变了。
关键判断点:getter 返回的是值(如 int、String)还是引用?引用是否可变?
- 基本类型和
String、LocalDateTime等不可变类 → 安全 - 返回
new ArrayList(this.list)或Collections.unmodifiableList(this.list)→ 才真正隔离修改 - 字段本身没加
final修饰 → 构造后仍可能被反射或内部逻辑改写(虽不常见,但破坏只读契约)
用 Collections.unmodifiableXXX 包装集合字段
这是最轻量、零依赖的方案,适合 JDK 1.2+。它不复制数据,只是加一层“拒绝修改”的代理,性能开销极小,但注意:它只拦截 add/remove/clear 等操作,不阻止对元素本身的修改(比如返回一个 unmodifiableList,但元素是可变对象,调用方仍可 list.get(0).setName("x"))。
示例:
private final List<User> users = new ArrayList<>();
public List<User> getUsers() {
return Collections.unmodifiableList(users);
}
- 必须用
final修饰字段,否则构造后还能被重新赋值 - 不要在 getter 里每次 new 包装对象(如
new UnmodifiableRandomAccessList(...)),那是冗余对象创建 - 对应 Map/Set 同理:用
Collections.unmodifiableMap、unmodifiableSet
用 Guava 的 ImmutableXXX 替代原始集合
如果你能引入 Guava(v21+),ImmutableList、ImmutableMap 是更彻底的选择:它们不仅禁止修改结构,还强制要求元素不可变(或至少不对外暴露可变引用),且自带 null 检查和线程安全语义。
示例:
private final ImmutableList<String> tags;
public MyClass(List<String> input) {
this.tags = ImmutableList.copyOf(input); // 自动防御 null 和并发修改
}
public ImmutableList<String> getTags() {
return tags;
}
-
ImmutableList.copyOf()会立即拷贝并冻结,比unmodifiableList更早暴露问题(比如输入含 null) - 不能用
ImmutableList.of(...)直接传原始集合,它不复制,仅包装 —— 这和Collections版本一样脆弱 - 如果元素本身可变(如
User),需确保User也是不可变类,或返回其不可变视图(如用 builder 模式封装)
getter 不够,字段和构造过程也得锁死
只靠 getter 防御是片面的。真正的只读类需要三重约束:字段 final + 构造时完成初始化 + 不提供任何 setter 或修改方法。哪怕一个 reset() 方法存在,整个类就不再是只读的。
- 所有字段必须声明为
final,包括集合、数组、嵌套对象 - 构造函数里完成全部赋值,避免“先 new 再 set”这种两阶段初始化
- 如果字段是数组(
int[]、User[]),getter 必须返回Arrays.copyOf(arr, arr.length),因为数组永远可变,unmodifiableList对原始数组无效 - 避免返回
this或内部对象的裸引用;必要时用 record(Java 14+)或 builder 模式封装不可变视图
最容易被忽略的一点:开发者常以为“我没写 setter 就安全了”,却忘了集合字段的 getter 返回的是活引用,而数组连包装层都没有 —— 这两类必须手动防御,没有捷径。

















