1. 概述

在 Java 中创建不可变的值对象(Value Object)时,虽然语义清晰、线程安全,但往往伴随着大量样板代码。更关键的是,Java 标准库中的集合类型(如 ListSet)本身是可变的,一旦被暴露,就可能破坏值对象的不可变性。

本文将介绍如何在使用 AutoValue 时,为集合字段创建防御性拷贝(Defensive Copy),从而确保值对象真正不可变。AutoValue 是一个注解处理器工具,能自动生成构造器、equalshashCode 等样板代码,极大简化不可变对象的定义。

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 会生成其具体实现,完成最终对象构造。
  • 将方法设为 abstractpackage-private,确保只有当前类能使用,不污染公共 API。

⚠️ 踩坑提醒:必须先读取原值,再创建拷贝,然后调用 setter 覆盖,最后调用 autoBuild()。顺序不能错,否则可能触发并发问题或空指针。

5. 总结

  • ✅ AutoValue 不会自动为集合做防御性拷贝,必须手动处理
  • ✅ 在静态工厂方法中:传参前调用 List.copyOf() 或类似方法。
  • ✅ 在 Builder 模式中:通过添加包私有抽象方法,结合自定义 build() 实现拷贝逻辑。
  • ✅ 优先使用 Java 10+ 的 List.copyOf(),性能更好,代码更简洁。
  • ⚠️ 所有可变引用类型(集合、数组、可变对象)都应考虑防御性拷贝,String 不需要。

示例代码已上传至 GitHub:https://github.com/example-java/autovalue-defensive-copy


原始标题:Defensive Copies for Collections Using AutoValue