AutoValue 探究(一)

AutoValue 是最常见的代码生成库,用法简单却经典,而且常常成为其他开源库的依赖,以实现更加复杂的功能。所以,细致的学习 AutoValue 的使用是很有必要的。 本文对 AutoValue 的应用范围、特点、基本用法进行简单的介绍。

{% blockquote AutoValue Document https://github.com/google/auto/blob/master/value/README.md %} AutoValue provides an easier way to create immutable value classes, with a lot less code and less room for error, while not restricting your freedom to code almost any aspect of your class exactly the way you want it. {% endblockquote %}

程序离不开数据,而 Java 项目中数据常见的表现形式就是类,且常常被用“值类”的方式表示。而所谓值类,则要求值类的任意两个不同的对象,如果对应的域都相等,则两个对象相等,且程序中它们是可以完全互换的。 而要使用 Java 语言实现符合要求的值类,则要编写大量格式相似的代码,比如结构相似的 equalshashCodetoString 方法。如果值类涉及其他的业务内容,虽然是其结构并不会有特别夸张的变化,但代码量仍然会极速膨胀。高重复度的大量代码会让人极度厌倦编程工作,而且编写错误也会成比例增加(错误率也可能上升)。 常见的重复代码的编写解决方案有两个:

  • IDE 智能填充文字内容
  • 第三方库生成代码 而本文介绍的 AutoValue 则是最常见的代码生成开源库。

{% blockquote Joshua Bloch, Effective Java %} AutoValue is a great tool for eliminating the drudgery of writing mundane value classes in Java. It encapsulates much of the advice in Effective Java Chapter 2, and frees you to concentrate on the more interesting aspects of your program. The resulting program is likely to be shorter, clearer, and freer of bugs. Two thumbs up. {% endblockquote %}

首先,选择第三方库而非 IDE 智能填充构造值类的原因有:

  • IDE 填充功能视工具(包含插件)不同而有极大差别,使用方式也不尽相同,应用范围较窄
  • IDE 填充功能常常需要大量针对性的模板设置,使用不够灵活,且学习成本较高
  • 第三方库生成代码可利用许多 Java 的强大功能,如:注解、反射等
  • 第三方库可以各自配合,实现更加复杂的功能

而后,使用 AutoValue 而非其他开源库的原因有:

  • 知名度最高,易搜集教程,易获得帮助
  • 背靠 Google,技术力量雄厚,代码稳定
  • 使用简单,文档清晰

要在 Gradle 的配置文件中添加如下内容,以使用相关依赖:

1
2
3
4
dependencies {
  compileOnly "com.google.auto.value:auto-value:1.2"
  apt         "com.google.auto.value:auto-value:1.2"
}

然后,对应想要生成的值类内容,构造抽象类,抽象类使用同名抽象方法对应值类域,比如: 对应如下值类(省略了所有修饰符和必要的方法):

1
2
3
4
class Animal {
    String name;
    int numberOfLegs;
}

应该有如下抽象类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import com.google.auto.value.AutoValue;

@AutoValue
abstract class Animal {
    static Animal create(String name, int numberOfLegs) {
    // See "How do I...?" below for nested classes.
    return new AutoValue_Animal(name, numberOfLegs);
    }

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

然后,build 之后会自动生成名为 AutoValue_Animal 的类,实现值类的具体内容,可能的生成内容如下(不同环境下内容可能不同):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
mport javax.annotation.Generated;

@Generated("com.google.auto.value.processor.AutoValueProcessor")
final class AutoValue_Animal extends Animal {
    private final String name;
    private final int numberOfLegs;

    AutoValue_Animal(String name, int numberOfLegs) {
        if (name == null) {
            throw new NullPointerException("Null name");
        }
        this.name = name;
        this.numberOfLegs = numberOfLegs;
    }

    @Override
    String name() {
        return name;
    }

    @Override
    int numberOfLegs() {
        return numberOfLegs;
    }

    @Override
    public String toString() {
        return "Animal{"
            + "name=" + name + ", "
            + "numberOfLegs=" + numberOfLegs + "}";
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) {
            return true;
        }
        if (o instanceof Animal) {
            Animal that = (Animal) o;
            return this.name.equals(that.name())
              && this.numberOfLegs == that.numberOfLegs();
        }
        return false;
    }

    @Override
    public int hashCode() {
        int h = 1;
        h *= 1000003;
        h ^= this.name.hashCode();
        h *= 1000003;
        h ^= this.numberOfLegs;
        return h;
    }
}

可以很明显的看出,生成的代码有如下特点:

  • 类有默认可见性(包可见)
  • 域对应抽象类的抽象访问方法,且为 final 类型
  • 使用构造器初始化域,无 setter
  • 实现全部抽象访问方法,返回对应域,实现 getter 功能
  • 生成一个 equals 方法,逐项比较域的内容
  • 生成一个 hashCode 方法,使用所有域的内容生成相应哈希码
  • 生成一个 toString 方法,以合适的格式返回所有域的内容

如果要使用这个类则可以使用如下方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public void testAnimal() {
    Animal dog = Animal.create("dog", 4);
    assertEquals("dog", dog.name());
    assertEquals(4, dog.numberOfLegs());

    // You probably don't need to write assertions like these; just illustrating.
    assertTrue(Animal.create("dog", 4).equals(dog));
    assertFalse(Animal.create("cat", 4).equals(dog));
    assertFalse(Animal.create("dog", 2).equals(dog));

    assertEquals("Animal{name=dog, numberOfLegs=4}", dog.toString());
}

一般来说,每次修改 @AutoValue 标注的抽象类,都要重新 build 以生成正确的实现类。如果抽象类内容需要反复修改,那么花在 build 的时间将非常恐怖,而且会极其严重地影响工作的正常进行。所以,最好在设计好值类的内容之后,再考虑编码实现它。 另外,如果其他库的功能对 AutoValue 的生成文件有依赖,或者其他代码不得不使用实现类而非抽象类,那么一旦抽象类需要重构则代价极为高昂,而且也难以使用 IDE 的智能重构功能辅助。 最后,每次使用 @AutoValue 注解,都要编译生成新的代码文件,如果项目较大则极其耗时,考虑使用 Gradle 插件等方法避免编译所有文件。

AutoValue 探究(二)

Android Model正确使用姿势 —— AutoValue