Ryan Harter

Freelance Android Developer

A Deeper Look at AutoValue

| Comments

In my last article, I gave a basic introduction to AutoValue, the code generating annotation processor that makes immutable value types in Java easy. Now I’d like to take a bit of a deeper look at AutoValue and how it works.

Compile Time Annotation Processing

First things first, AutoValue is a compile time annotation processor. This means that it only runs when you compile your code, as opposed to when your app is running. This has a few implications for your app, namely that including AutoValue doesn’t significantly effect your application’s performance or size.

The reason I say it won’t significantly effect your app is because it does add generated code to your final binary. This generally isn’t something you need to be worried about because, if you’re making value types, this is the code you should be generating anyway. As an added bonus, since AutoValue is tested, you can trust that there won’t be errors in the generated code and it will be performant.

If you’re not careful, however, it can be easy to accidentally include AutoValue, and all of it’s shaded dependencies, in your final binary. If, like me, you are an Android developer, this can have a serious impact on your app and users.

The correct way to include AutoValue so that it doesn’t bloat your app is to add it to the right configuration so that it only gets included in the annotation processing phase of your build process.

For Android, you’ll have to install the android-apt gradle plugin, then add AutoValue to the apt configuration. You’ll also have to add it to the provided configuration in order to get proper code completion.

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

Note This is a really important step for Android apps, as you will easily cross the dex method limit if you don’t handle this right.

To use AutoValue with Gradle in a java project, you can install the apt plugin and add the dependencies to the compileOnly and apt configurations.

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

Maven users can simply add the dependency to the provided scope.

1
2
3
4
5
6
<dependency>
  <groupId>com.google.auto.value</groupId>
  <artifactId>auto-value</artifactId>
  <version>1.2</version>
  <scope>provided</scope>
</dependency>

The need to list the dependency in multiple configurations is because, unlike most annotations processors, AutoValue includes both the annotation and annotation processor in the same artifact. There is already an issue to fix this, and the fix will remove the need for listing the same dependency in multiple configurations.

Scanning Your classes

Once you have AutoValue added as a dependency, it will run when you compile your app to process all classes annotated with @AutoValue. For a general overview of how annotation processors work in Java, check out Hannes Dorfmann’s great article, Annotation Processing 101.

For each @AutoValue annotated class, AutoValue first looks for all abstract methods to determine what it should generate. It does this by stepping through all abstract methods in your class, including those inherited by interfaces, which take no arguments and return a value. These methods are considered properties, and AutoValue will generate appropriate fields and include them in the equals, hashCode and toString methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@AutoValue public abstract class Foo {

  // This method takes no arguments and returns
  // String, so it's a property
  public abstract String foo();

  // This method is not abstract, so it's not
  // a property.
  public boolean hasValues() {
    return foo() != null && !foo().isEmpty();
  }

  // This method takes a parameter and returns void,
  // so it's not a property.  Hopefully an extension
  // generates the implementation.
  public abstract void writeValues(OutputStream out);
}

Since AutoValue knows the difference between properties and your custom method, this means that you can have methods that return generated values, or perform other operations on your object.

Also, it’s important note that your property methods don’t need to be public. While they can’t be private as they have to be generated by the AutoValue subclass, they can be package private or protected to keep them hidden from your public API.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@AutoValue public abstract class User {

  // Package private property won't be
  // visible outside of the package, but
  // will still be generated.
  abstract String internalFirstName();

  // Publicly available method that uses the
  // internally generated one.
  public String firstName() {
    String name = internalFirstName().trim();
    return Character.toUpperCase(name.charAt(0)) + name.substring(1);
  }
}

Another thing to consider here is equals, hashCode and toString. If you want to implement your own version of any of these methods, you can simply do that in your annotated class, and AutoValue will recognize that and won’t generate it’s own, meaning your implementation will be inherited and used in the final class.

1
2
3
4
5
6
7
8
9
10
11
12
13
@AutoValue public abstract class User {
  public abstract String firstName();
  public abstract String lastName();
  public abstract int age();

  // I'm the same person as me from 10 years ago...or am I?
  @Override public boolean equals(Object other) {
    if (!(other instanceof User)) return false;
    User o = (User) other;
    return firstName().equals(o.firstName())
        && lastName().equals(o.lastName());
  }
}

Understanding the way AutoValue defines properties adds a lot of flexibility to your classes.

The Generated Code

As I mentioned in the previous article, AutoValue generates immutable value types by creating a constructor, member variables and their getters (properties), and implementing equals, hashCode and toString, according to the rules outlined above. Let’s take a closer look at the generated class one piece at a time.

The Class

1
2
3
4
5
6
7
package com.example.autovalue;

final class AutoValue_User extends User {

  ...

}

The first thing to notice about the generated class is that it subclasses the annotated abstract class. This means that in your consuming code, you can simply pass around User objects, without anyone ever having to know that you use AutoValue to generate the implementation. This is intentional, and makes it easy to change the underlying implementation if AutoValue isn’t serving your needs.

Another small piece to consider is that the final generated class will always be the annotated class name prefixed with AutoValue_. This is important in case you need to access generated code within your annotated class, like accessing the constructor in a static factory method.

Lastly, notice that the generated class is package private and final. This means that the AutoValue generated class isn’t visible outside your package, and shouldn’t ever need to be used by anything other than your annotated base class.

The Constructor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
final class AutoValue_User extends User {

  AutoValue_User(
      String firstName,
      String lastName,
      int age) {
    if (firstName == null) {
      throw new NullPointerException("Null firstName");
    }
    this.firstName = firstName;
    if (lastName == null) {
      throw new NullPointerException("Null lastName");
    }
    this.lastName = lastName;
    this.age = age;
  }

}

The generated constructor, again package private, takes as parameters all of the properties, in the order in which they were defined in the annotated class.

The arguments sent in are null checked within the constructor. All properties are considered non-nullable by default. If you want to make a property nullable, you can simply add an @Nullable annotation to it.

1
2
3
@AutoValue public abstract class User {
  @Nullable public abstract String middleName();
}

The Member variables

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final class AutoValue_User extends User {

  private final String firstName;
  private final String lastName;
  private final int age;

  @Override
  public String firstName() {
    return firstName;
  }

  @Override
  public String lastName() {
    return lastName;
  }

  @Override
  public int age() {
    return age;
  }

}

There really isn’t anything special about the member variables and the associated method implementations, except that the fields are final, to enforce the immutability of the type.

equals(), hashCode() and toString()

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
final class AutoValue_User extends User {

  @Override
  public String toString() {
    return "User{"
        + "firstName=" + firstName + ", "
        + "lastName=" + lastName + ", "
        + "age=" + age
        + "}";
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (o instanceof User) {
      User that = (User) o;
      return (this.firstName.equals(that.firstName()))
           && (this.lastName.equals(that.lastName()))
           && (this.age == that.age());
    }
    return false;
  }

  @Override
  public int hashCode() {
    int h = 1;
    h *= 1000003;
    h ^= this.firstName.hashCode();
    h *= 1000003;
    h ^= this.lastName.hashCode();
    h *= 1000003;
    h ^= this.age;
    return h;
  }

}

Lastly, we have equals(), hashCode() and toString(), which are also quite default implementations.

Conclusion

As you can see, there is a lot that AutoValue is doing for us, but it is smart enough to infer what we want it to do and make reasonable decisions. This allows a lot of flexibility in the classes you write, as, for the most part, you can override what AutoValue would normally do by simply writing the code you would have anyway.

This, along with the fact that AutoValue itself is entirely hidden from end users, and the fact that it doesn’t add any unnecessary overhead to your project, make it a great fit for most apps.

Comments