Contents

AutoValue 简介

1. 概述

AutoValue 是 Java 的源代码生成器,更具体地说,它是一个用于为值对象或值类型对象生成源代码的库。

为了生成一个值类型的对象,你所要做的就是用***@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>

最新版本可以通过这个链接 找到。

3. 值类型对象

值类型是库的最终产品,因此要了解它在我们的开发任务中的位置,我们必须彻底了解值类型,它们是什么,它们不是什么以及我们为什么需要它们。

3.1. 什么是价值类型?

值类型对象是彼此之间的平等不是由身份决定的,而是由它们的内部状态决定的。这意味着值类型对象的两个实例被认为是相等的,只要它们具有相等的字段值。

通常,值类型是不可变的。它们的字段必须是final的,并且它们不能具有setter方法,因为这将使它们在实例化后可以更改。

它们必须通过构造函数或工厂方法使用所有字段值。

值类型不是 JavaBean,因为它们没有默认或零参数构造函数,也没有 setter 方法,同样,它们不是 Data Transfer Objects 也不是 Plain Old Java Objects

此外,值类型的类必须是最终的,因此它们不可扩展,至少有人会覆盖这些方法。JavaBeans、DTOs 和 POJOs 不必是最终的。

3.2. 创建值类型

假设我们要创建一个名为Foo的值类型,其中包含名为textnumber的字段。我们将如何去做?

我们将创建一个 final 类并将其所有字段标记为 final。然后我们将使用 IDE 生成构造函数、*hashCode()方法、equals(Object)方法、作为强制方法的gettertoString()*方法,我们将有一个像这样的类:

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;
    }
}

创建Foo的实例后,我们希望它的内部状态在其整个生命周期内保持不变。

正如我们将在下面的小节中看到的,对象的hashCode必须从 instance 更改为 instance,但是对于值类型,我们必须将其绑定到定义值对象内部状态的字段。

因此,即使更改同一对象的字段也会更改hashCode值。

3.3. 值类型如何工作

值类型必须是不可变的原因是为了防止应用程序在实例化后对其内部状态进行任何更改。

每当我们想要比较任何两个值类型的对象时,*我们都必须使用Object类的*equals(Object)方法

这意味着我们必须始终在我们自己的值类型中覆盖此方法,并且仅当我们正在比较的值对象的字段具有相等值时才返回 true。

此外,为了让我们在HashSetHashMap等基于哈希的集合中使用我们的值对象而不会中断,*我们必须正确实现*hashCode()方法

3.4. 为什么我们需要价值类型

对值类型的需求经常出现。在这些情况下,我们希望覆盖原始Object类的默认行为。

正如我们已经知道的,Object类的默认实现认为两个对象在它们具有相同标识时是相等的,但是出于我们的目的,我们认为两个对象在它们具有相同的内部状态时是相等的

假设我们要创建一个money对象,如下所示:

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));
}

注意测试的语义。

当两个货币对象不相等时,我们认为它已经过去了。这是因为我们没有重写equals方法,所以相等性是通过比较对象的内存引用来衡量的,这当然不会有所不同,因为它们是不同的对象占用不同的内存位置。

每个对象代表 10,000 美元,但Java 告诉我们我们的货币对象不相等。我们希望这两个对象仅在货币金额不同或货币类型不同时测试不相等。

现在让我们创建一个等效值对象,这一次我们将让 IDE 生成大部分代码:

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;
    }
}

唯一的区别是我们覆盖了*equals(Object)hashCode()*方法,现在我们可以控制我们希望 Java 如何比较我们的货币对象。让我们运行它的等效测试:

@Test
public void givenTwoSameValueMoneyValueObjects_whenEqualityTestPasses_thenCorrect() {
    ImmutableMoney m1 = new ImmutableMoney(10000, "USD");
    ImmutableMoney m2 = new ImmutableMoney(10000, "USD");
    assertTrue(m1.equals(m2));
}

注意这个测试的语义,当两个货币对象通过equals方法测试相等时,我们希望它通过。

4. 为什么选择 AutoValue?

现在我们彻底理解了值类型以及为什么需要它们,我们可以看看 AutoValue 以及它是如何进入等式的。

4.1. 手工编码的问题

当我们像上一节那样创建值类型时,我们会遇到许多与糟糕设计和大量样板代码相关的问题。

一个两字段类将有 9 行代码:一行用于包声明,两行用于类签名及其右大括号,两行用于字段声明,两行用于构造函数及其右大括号,两行用于初始化字段,但是我们需要 getter对于字段,每个字段多写三行代码,多出六行。

覆盖*hashCode()equalTo(Object)方法分别需要大约 9 行和 18 行,而覆盖toString()*方法又增加了 5 行。

这意味着我们的两个字段类的格式良好的代码库将需要大约 50 行代码

4.2. IDE 的救援?

使用 Eclipse 或 IntilliJ 之类的 IDE 并且只需创建一个或两个值类型的类,这很容易。想一想要创建大量这样的类,即使 IDE 帮助我们,它是否仍然那么容易?

快进,几个月后,假设我们必须重新访问我们的代码并修改我们的Money类,并且可能将currency字段从String类型转换为另一种称为Currency 的值类型。

4.3. IDE 并没有那么有用

像 Eclipse 这样的 IDE 不能简单地为我们编辑访问器方法,也不能简单地编辑toString()、*hashCode()equals(Object)*方法。

这种重构必须手动完成。编辑代码增加了潜在的错误,并且随着我们添加到Money类的每个新字段,行数呈指数增长。

认识到这种情况会发生,它经常大量发生,这将使我们真正意识到 AutoValue 的作用。

5. AutoValue 示例

AutoValue 解决的问题是将我们在上一节中讨论过的所有样板代码排除在外,这样我们就不必编写、编辑甚至阅读它。

我们将看同样的Money示例,但这次是 AutoValue。为了一致性起见,我们将这个类称为AutoValueMoney

@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);
    }
}

发生的事情是我们编写了一个抽象类,为它定义了抽象访问器但没有字段,我们用*@AutoValue*注解类,总共只有 8 行代码,javac为我们生成一个具体的子类,如下所示:

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;
    }
}

我们根本不需要直接处理这个类,当我们需要添加更多字段或对我们的字段进行更改时,我们也不需要编辑它,就像上一节中的currency场景一样。

Javac总是会为我们重新生成更新的代码

在使用这个新的值类型时,所有调用者看到的只是父类型,我们将在下面的单元测试中看到。

这是一个验证我们的字段设置正确的测试:

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

验证两个具有相同货币和相同金额测试相等的AutoValueMoney对象的测试如下:

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

当我们将一个货币对象的货币类型更改为 GBP 时,测试:5000 GBP == 5000 USD不再成立:

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

6. AutoValue架构

我们看到的初始示例涵盖了使用静态工厂方法作为公共创建 API 的 AutoValue 的基本用法。

**请注意,如果我们所有的字段都是Strings,**那么在我们将它们传递给静态工厂方法时很容易互换它们,例如将money放在currency的位置,反之亦然。

如果我们有很多字段并且都是string类型,这种情况尤其可能发生。由于使用 AutoValue,所有字段都通过构造函数初始化,因此这个问题变得更糟。

为了解决这个问题,我们应该使用Builder模式。幸运的是。这可以由 AutoValue 生成。

我们的 AutoValue 类并没有真正改变太多,只是静态工厂方法被一个构建器替换:

@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();
    }
}

生成的类与第一个类相同,但生成了构建器的具体内部类,并在构建器中实现了抽象方法:

static final class Builder extends AutoValueMoneyWithBuilder.Builder {
    private String currency;
    private long amount;
    Builder() {
    }
    Builder(AutoValueMoneyWithBuilder source) {
        this.currency = source.getCurrency();
        this.amount = source.getAmount();
    }
    
    @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));
}