1. 概述
在 Java 中创建不可变的值对象(Value Object)时,虽然语义清晰、线程安全,但往往伴随着大量样板代码。更关键的是,Java 标准库中的集合类型(如 List
、Set
)本身是可变的,一旦被暴露,就可能破坏值对象的不可变性。
本文将介绍如何在使用 AutoValue 时,为集合字段创建防御性拷贝(Defensive Copy),从而确保值对象真正不可变。AutoValue 是一个注解处理器工具,能自动生成构造器、equals
、hashCode
等样板代码,极大简化不可变对象的定义。
2. 值对象与防御性拷贝
值对象的核心特征是:不可变性 + 由值决定相等性。它通常用于封装一组相关数据,比如用户信息、配置项等。
考虑以下 Person
类:
class Person {
private final String name;
private final List<String> favoriteMovies;
// 构造器、getter、toString、equals、hashCode 省略
}
乍看之下,final
字段似乎保证了不可变性。但问题出在 List<String>
上 —— 它只是引用不可变,内容仍可被外部修改:
var favoriteMovies = new ArrayList<String>();
favoriteMovies.add("Clerks"); // fine
var person = new Person("Katy", favoriteMovies);
favoriteMovies.add("Dogma"); // oh, no! person 的 favoriteMovies 被悄悄修改了!
✅ 解决方案:在构造时进行防御性拷贝
public Person(String name, List<String> favoriteMovies) {
this.name = name;
this.favoriteMovies = List.copyOf(favoriteMovies); // Java 10+
}
⚠️ 注意:String
是不可变类型,无需拷贝。只有可变引用类型(如集合、数组、自定义可变对象)才需要防御性拷贝。
📌 兼容旧版本 Java 的写法(Java 9 之前):
this.favoriteMovies = Collections.unmodifiableList(new ArrayList<>(favoriteMovies));
两种方式效果类似,但 List.copyOf()
更简洁高效,推荐优先使用。
3. AutoValue 与防御性拷贝
AutoValue 能自动生成 Person
的实现类(如 AutoValue_Person
),但 ❌ 它不会自动为你做防御性拷贝。这意味着如果你直接传入一个可变集合,依然存在被外部修改的风险。
使用 AutoValue 的典型写法:
@AutoValue
public abstract class Person {
public static Person of(String name, List<String> favoriteMovies) {
return new AutoValue_Person(name, favoriteMovies);
}
public abstract String name();
public abstract List<String> favoriteMovies();
}
上面的 of
方法直接将原始 favoriteMovies
传入生成的构造器,没有做任何保护。
✅ 正确做法:在静态工厂方法中手动做拷贝
public static Person of(String name, List<String> favoriteMovies) {
// ✅ 先拷贝,再构造
var favoriteMoviesCopy = List.copyOf(favoriteMovies);
return new AutoValue_Person(name, favoriteMoviesCopy);
}
这样,即使调用者后续修改了传入的列表,也不会影响已创建的 Person
实例。
4. AutoValue Builder 与防御性拷贝
当使用 @AutoValue.Builder
时,情况更复杂一些。Builder 模式允许逐步设置字段,最后调用 build()
创建实例。由于 AutoValue 自动生成了 Builder
的实现,我们无法直接干预字段赋值过程。
典型 Builder 结构:
@AutoValue
public abstract class Person {
public abstract String name();
public abstract List<String> favoriteMovies();
public static Builder builder() {
return new AutoValue_Person.Builder();
}
@AutoValue.Builder
public static class Builder {
public abstract Builder name(String value);
public abstract Builder favoriteMovies(List<String> value);
public abstract Person build();
}
}
问题来了:favoriteMovies(List<String> value)
接收的仍是原始引用,build()
前如何插入防御性拷贝?
✅ 解决方案:混合使用自动生成与自定义代码
我们通过添加两个 包私有(package-private)抽象方法 来“钩住”生成逻辑:
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder name(String value);
public abstract Builder favoriteMovies(List<String> value);
// 用于获取当前 builder 中的 favoriteMovies 值
abstract List<String> favoriteMovies();
// 调用 AutoValue 生成的 build 逻辑
abstract Person autoBuild();
// 自定义 build 方法,插入防御性拷贝
public Person build() {
List<String> original = favoriteMovies();
List<String> copy = List.copyOf(original);
// 替换为拷贝后的不可变列表
favoriteMovies(copy);
return autoBuild();
}
}
📌 关键点解释:
favoriteMovies()
(无参)是 AutoValue 生成的 getter,用于读取当前 builder 状态。favoriteMovies(List<String>)
(有参)是 setter,我们用它把拷贝后的列表重新设回去。autoBuild()
是我们约定的名称,AutoValue 会生成其具体实现,完成最终对象构造。- 将方法设为
abstract
和package-private
,确保只有当前类能使用,不污染公共 API。
⚠️ 踩坑提醒:必须先读取原值,再创建拷贝,然后调用 setter 覆盖,最后调用 autoBuild()
。顺序不能错,否则可能触发并发问题或空指针。
5. 总结
- ✅ AutoValue 不会自动为集合做防御性拷贝,必须手动处理。
- ✅ 在静态工厂方法中:传参前调用
List.copyOf()
或类似方法。 - ✅ 在 Builder 模式中:通过添加包私有抽象方法,结合自定义
build()
实现拷贝逻辑。 - ✅ 优先使用 Java 10+ 的
List.copyOf()
,性能更好,代码更简洁。 - ⚠️ 所有可变引用类型(集合、数组、可变对象)都应考虑防御性拷贝,String 不需要。
示例代码已上传至 GitHub:https://github.com/example-java/autovalue-defensive-copy