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 手动创建值类型
假设要创建包含 text
和 number
字段的值类型 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 的局限性
当需要修改字段类型(如 currency
从 String
改为 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 模式提升可读性
- 字段变更时零维护成本
完整示例代码可在 AutoValue GitHub 项目 中获取。