@AutoValue
... what, why and how?
Kevin Bourrillion & Éamonn McManus
Google, Inc.
What's a "value type"?
By this we mean a "type with value semantics."
It's class without identity: two instances are considered interchangeable as long as they have equal field values.
Examples: DateTime, Money, Uri... but you also tend to create a great many of these yourself.
You know the kind: they're the ones where you have to implement equals and hashCode, and usually toString.
Note: "value type" can mean different things in different contexts. This slide defines how we are using it in the remainder of this overview.
So they're structs?
A common misconception is that value types never have behavior, only state—they're structs.
This is like assuming an enum is only an int. Java's enums are much richer than that!
In value types, as with enums, behavior is welcome! (This behavior should be limited to pure functions that depend only on their inputs and instance state).
Java, you have a problem.
How much trouble could it be to create one of these value types?
Ignoring whatever actual behavior you need, let's just start by coding up the basic skeleton...
WAT
1 public final class Foo {
2 private final String text;
3 private final int number;
4
5 public Foo(String text, int number) {
6 // defensive copies, preconditions
7 this.text = text;
8 this.number = number;
9 }
10
11 /** Documentation here. */
12 public String text() {
13 return text;
14 }
15
16 /** Documentation here. */
17 public int number() {
18 return number;
19 }
20
21 @Override public boolean equals(@Nullable Object o) {
22 if (o instanceof Foo) {
23 Foo that = (Foo) o;
24 return text.equals(that.text)
25 && number == that.number;
26 }
27 return false;
28 }
29
30 @Override public int hashCode() {
31 return Objects.hashCode(text, number);
32 }
33
34 @Override public String toString() {
35 return Objects.toStringHelper(this)
36 .add("text", text)
37 .add("number", number)
38 .toString();
39 }
40 }
That is a lot of code!
Why so much code?
Quite simply, if something feels like a value object, it's generally considered evil to not include all this stuff.
Solutions
We'll now go through a number of common solutions to this problem, and explain why we don't like them.
Then we'll show @AutoValue and how it avoids nearly all of the pitfalls of the other approaches.
Solution 0: just hand-code it
Are changes really that risky?
Suppose a required int field becomes optional. You change
int number;
to
Integer number;
Tests still pass... and everything's fine... until one day, objects put into Sets start mysteriously "disappearing" from the set! Why? (Left as an exercise to the reader.)
Solution 0: we don't like it
Don't make the common mistake of considering only cost of initial development. Many costs come later, as we just listed.
And there are also second-order costs:
Solution 1: fix in the language?
JDK 9 could perhaps be out in 2016. With luck you could be using it in 2017?
It's possible it could bring relief, but we'd rather have a solution now!
New JVM languages are in general promising, but we'll have tons of code in Java for a long time no matter what.
Solution 2: IDE templates
Eclipse and IntelliJ can spew these out for you.
But all this does is speed up initial development a bit, and sidestep most initial errors.
It's copy-and-paste coding!
It addresses none of the other problems of "Solution 0", which are really the more significant ones.
Solution 3: Tuple base classes
Imagine abstract base classes like Tuple2<A,B>, Tuple3<A,B,C>, etc.
public final class MyType extends Tuple3<String, Integer, MyEnum> {
public MyType(String s, int i, MyEnum e) {
super(s, i, e);
// check preconditions
}
public String name() { return first(); }
public int id() { return second(); }
public MyEnum myEnum() { return third(); }
}
Solution 3: problems
Solution 4: reflective base class
public final class Foo extends AbstractImmutableReflectiveValueThing {
private final String text;
private final int number;
/** Documentation here. */
public Foo(String text, int number) {
// defensive copies, preconditions
this.text = text;
this.number = number;
}
public String text() { // optional
return text;
}
}
Costs of the reflective base class
Now what?
Do we have to choose between convenience and performance?
To get both at the same time, we conclude that code generation of one form or another is going to be the only way out. So....
Solution 5: Protocol Buffers
This use case isn't a design goal for protocol buffers.
Solution 6: Codegen from DSL
Configure fields in our special language, then generate Java code from that -- a very conventional kind of codegen.
Solution 7: Project Lombok
With Lombok, a class with @Value need only declare fields. Lombok hacks the Java compiler to insert the constructor, getters, equals, hashCode, and toString into the class as it is compiled.
Introducing AutoValue
Our new solution!
What you write (plain Java)
import com.google.auto.value.AutoValue;
@AutoValue
public abstract class Foo {
public static Foo create(String text, int number) {
// defensive copies, preconditions
return new AutoValue_Foo(text, number);
}
/** Documentation here. */
public abstract String text(); // or getText(), if you like
/** Documentation here. */
public abstract int number();
}
What code does that generate?
final class AutoValue_Foo extends Foo { // (note: NOT public!)
private final String text;
private final int number;
AutoValue_Foo(String text, int number) {
if (text == null) {
throw new NullPointerException("text");
}
this.text = text;
this.number = number;
}
@Override public String text() {
return text;
}
@Override public int number() {
return number;
}
// and the customary equals/hashCode/toString you'd have written.
// I don't even feel like typing that garbage out on a slide...
}
Advantages, part 1
Plus all the flexibility of code-by-hand:
Disadvantages
The biggest problem
AutoValue does introduce some fragility.
The generator has to choose the order of constructor parameters somehow, so it uses the order in which the accessors appear in the source file.
This means an innocent refactoring to reorder those accessors could break your tests. (You do have tests that actually do stuff with your value objects, right?)
AutoValue users’ guide