AutoValue 是一个 Java 源代码生成器,专门用于生成值对象(value objects)或值类型对象(value-typed objects)。使用方式非常简单:只需用 @AutoValue 注解标注一个抽象类,编译后就会自动生成包含访问器方法、参数化构造函数、以及正确重写的 toString()equals(Object)hashCode() 方法的值对象。

下面是一个快速示例,展示如何通过抽象类生成名为 AutoValue_Person 的值对象:

@AutoValue
abstract class Person {
    static Person create(String name, int age) {
        return new AutoValue_Person(name, age);
    }

    abstract String name();
    abstract int age();
}

接下来我们将深入探讨值对象的核心概念、AutoValue 的优势以及实际应用场景。

2. Maven 配置

在 Maven 项目中使用 AutoValue,只需在 pom.xml 中添加以下依赖:

<dependency>
    <groupId>com.google.auto.value</groupId>
    <artifactId>auto-value</artifactId>
    <version>1.2</version>
</dependency>

最新版本可通过 Maven 中央仓库 查询。

3. 值类型对象详解

值类型是 AutoValue 的核心产物,理解其设计原理和必要性至关重要。

3.1 什么是值类型?

值类型对象的相等性不基于对象身份(内存地址),而是由内部状态决定。只要两个对象的字段值完全相同,即视为相等。

核心特征:

  • 不可变性:所有字段必须为 final,禁止提供 setter 方法
  • 初始化方式:必须通过构造函数或工厂方法完成字段赋值
  • 类声明:必须为 final 类,防止被继承破坏方法约定
  • 不是 JavaBean/DTO/POJO:无默认构造函数,无 setter 方法

3.2 手动创建值类型

假设要创建包含 textnumber 字段的值类型 Foo,传统方式需要编写约 50 行样板代码:

public final class Foo {
    private final String text;
    private final int number;
    
    public Foo(String text, int number) {
        this.text = text;
        this.number = number;
    }
    
    // standard getters
    
    @Override
    public int hashCode() {
        return Objects.hash(text, number);
    }
    @Override
    public String toString() {
        return "Foo [text=" + text + ", number=" + number + "]";
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        Foo other = (Foo) obj;
        if (number != other.number) return false;
        if (text == null) {
            if (other.text != null) return false;
        } else if (!text.equals(other.text)) {
            return false;
        }
        return true;
    }
}

踩坑提示:值对象的 hashCode 必须与字段值绑定,任何字段变更都会导致哈希值变化。

3.3 值类型工作原理

值类型必须不可变,确保实例化后状态不被篡改。比较两个值对象时:

  • 必须使用重写的 equals(Object) 方法
  • 需正确实现 hashCode() 以支持哈希集合(如 HashSet/HashMap

3.4 为什么需要值类型?

当需要覆盖 Object 类的默认行为时,值类型就派上用场了。以货币对象为例:

public class MutableMoney {
    private long amount;
    private String currency;
    
    public MutableMoney(long amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }
    
    // standard getters and setters
}

测试会发现语义问题:两个金额相同的对象被判定为不等:

@Test
public void givenTwoSameValueMoneyObjects_whenEqualityTestFails_thenCorrect() {
    MutableMoney m1 = new MutableMoney(10000, "USD");
    MutableMoney m2 = new MutableMoney(10000, "USD");
    assertFalse(m1.equals(m2)); // 实际返回 false
}

使用值类型改造后,语义符合预期:

public final class ImmutableMoney {
    private final long amount;
    private final String currency;
    
    public ImmutableMoney(long amount, String currency) {
        this.amount = amount;
        this.currency = currency;
    }
    
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + (int) (amount ^ (amount >>> 32));
        result = prime * result + ((currency == null) ? 0 : currency.hashCode());
        return result;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        ImmutableMoney other = (ImmutableMoney) obj;
        if (amount != other.amount) return false;
        if (currency == null) {
            if (other.currency != null) return false;
        } else if (!currency.equals(other.currency))
            return false;
        return true;
    }
}

测试通过:

@Test
public void givenTwoSameValueMoneyValueObjects_whenEqualityTestPasses_thenCorrect() {
    ImmutableMoney m1 = new ImmutableMoney(10000, "USD");
    ImmutableMoney m2 = new ImmutableMoney(10000, "USD");
    assertTrue(m1.equals(m2)); // 正确返回 true
}

4. 为什么选择 AutoValue?

4.1 手动编码的痛点

手动创建值类型存在两大问题:

  • 设计缺陷:字段变更时需同步修改多个方法
  • 样板代码泛滥:两个字段的类需约 50 行代码(含 getter/equals/hashCode/toString)

4.2 IDE 能解决问题吗?

IDE 生成代码虽方便,但存在局限:

  • 适合少量类,大规模创建时效率低下
  • 字段变更时需手动重构所有相关方法

4.3 IDE 的局限性

当需要修改字段类型(如 currencyString 改为 Currency 对象)时:

  • IDE 无法自动更新访问器方法
  • 需手动修改 equals()/hashCode()/toString()
  • 每新增一个字段,代码量指数级增长

简单粗暴的结论:AutoValue 彻底解决了这些痛点!

5. AutoValue 实战

用 AutoValue 重构之前的货币类,只需 8 行代码:

@AutoValue
public abstract class AutoValueMoney {
    public abstract String getCurrency();
    public abstract long getAmount();
    
    public static AutoValueMoney create(String currency, long amount) {
        return new AutoValue_AutoValueMoney(currency, amount);
    }
}

编译后自动生成约 40 行实现代码(开发者无需关心):

public final class AutoValue_AutoValueMoney extends AutoValueMoney {
    private final String currency;
    private final long amount;
    
    AutoValue_AutoValueMoney(String currency, long amount) {
        if (currency == null) throw new NullPointerException(currency);
        this.currency = currency;
        this.amount = amount;
    }
    
    // standard getters
    
    @Override
    public int hashCode() {
        int h = 1;
        h *= 1000003;
        h ^= currency.hashCode();
        h *= 1000003;
        h ^= amount;
        return h;
    }
    
    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (o instanceof AutoValueMoney) {
            AutoValueMoney that = (AutoValueMoney) o;
            return (this.currency.equals(that.getCurrency()))
              && (this.amount == that.getAmount());
        }
        return false;
    }
}

核心优势:

  • 零维护成本:字段变更时自动重新生成
  • 类型安全:调用方只看到抽象父类

测试验证:

@Test
public void givenValueTypeWithAutoValue_whenFieldsCorrectlySet_thenCorrect() {
    AutoValueMoney m = AutoValueMoney.create("USD", 10000);
    assertEquals(m.getAmount(), 10000);
    assertEquals(m.getCurrency(), "USD");
}

@Test
public void given2EqualValueTypesWithAutoValue_whenEqual_thenCorrect() {
    AutoValueMoney m1 = AutoValueMoney.create("USD", 5000);
    AutoValueMoney m2 = AutoValueMoney.create("USD", 5000);
    assertTrue(m1.equals(m2));
}

@Test
public void given2DifferentValueTypesWithAutoValue_whenNotEqual_thenCorrect() {
    AutoValueMoney m1 = AutoValueMoney.create("GBP", 5000);
    AutoValueMoney m2 = AutoValueMoney.create("USD", 5000);
    assertFalse(m1.equals(m2));
}

6. 结合 Builder 模式

当字段较多且类型相同时(如多个 String 字段),静态工厂方法容易导致参数顺序错误。此时 Builder 模式更合适:

@AutoValue
public abstract class AutoValueMoneyWithBuilder {
    public abstract String getCurrency();
    public abstract long getAmount();
    static Builder builder() {
        return new AutoValue_AutoValueMoneyWithBuilder.Builder();
    }
    
    @AutoValue.Builder
    abstract static class Builder {
        abstract Builder setCurrency(String currency);
        abstract Builder setAmount(long amount);
        abstract AutoValueMoneyWithBuilder build();
    }
}

自动生成的 Builder 实现:

static final class Builder extends AutoValueMoneyWithBuilder.Builder {
    private String currency;
    private long amount;
    Builder() {}
    
    @Override
    public AutoValueMoneyWithBuilder.Builder setCurrency(String currency) {
        this.currency = currency;
        return this;
    }
    
    @Override
    public AutoValueMoneyWithBuilder.Builder setAmount(long amount) {
        this.amount = amount;
        return this;
    }
    
    @Override
    public AutoValueMoneyWithBuilder build() {
        String missing = "";
        if (currency == null) missing += " currency";
        if (amount == 0) missing += " amount";
        if (!missing.isEmpty()) {
            throw new IllegalStateException("Missing required properties:" + missing);
        }
        return new AutoValue_AutoValueMoneyWithBuilder(this.currency, this.amount);
    }
}

测试验证:

@Test
public void givenValueTypeWithBuilder_whenFieldsCorrectlySet_thenCorrect() {
    AutoValueMoneyWithBuilder m = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("USD").build();
    assertEquals(m.getAmount(), 5000);
    assertEquals(m.getCurrency(), "USD");
}

@Test
public void given2EqualValueTypesWithBuilder_whenEqual_thenCorrect() {
    AutoValueMoneyWithBuilder m1 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("USD").build();
    AutoValueMoneyWithBuilder m2 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("USD").build();
    assertTrue(m1.equals(m2));
}

@Test
public void given2DifferentValueTypesBuilder_whenNotEqual_thenCorrect() {
    AutoValueMoneyWithBuilder m1 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("USD").build();
    AutoValueMoneyWithBuilder m2 = AutoValueMoneyWithBuilder.builder()
      .setAmount(5000).setCurrency("GBP").build();
    assertFalse(m1.equals(m2));
}

7. 总结

AutoValue 通过自动生成不可变值类型,彻底解决了手动编写样板代码的痛点:

  • 减少约 80% 的重复代码
  • 保证 equals()/hashCode()/toString() 的正确实现
  • 支持 Builder 模式提升可读性
  • 字段变更时零维护成本

替代方案参考Lombok 项目 提供类似功能,可参考其入门教程

完整示例代码可在 AutoValue GitHub 项目 中获取。


原始标题:Introduction to AutoValue