1 dari 29

@AutoValue

... what, why and how?

Kevin Bourrillion & Éamonn McManus

Google, Inc.

2 dari 29

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.

3 dari 29

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).

4 dari 29

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...

5 dari 29

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 }

6 dari 29

That is a lot of code!

  • ~40 vertical lines (in Google style)
  • ... plus ~10 lines per additional field
  • ... and that's without mutators
  • ... and that's even using a few Guava helpers
  • ... and it's very formulaic code
  • ... and it does nothing to improve anyone's understanding of your code or domain
  • Worst: the need to do this comes up again,
    • and again,
      • and again,
        • and again.

7 dari 29

Why so much code?

  • Inherent redundancy of field/constructor/accessor
  • Without equals and hashCode, you can never...
  • put it in a HashSet
  • use it as a key in a HashMap, Cache, etc.
  • put it in any collection and have contains work
  • use assertEquals with it in tests.... etc.
  • Without toString, logging and debugging are painful.

Quite simply, if something feels like a value object, it's generally considered evil to not include all this stuff.

8 dari 29

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.

9 dari 29

Solution 0: just hand-code it

  • It's an egregious violation of D.R.Y.
  • Will you test these methods exhaustively?
    • If so... it tends to be a waste of your time
    • But if not... occasionally mistakes will slip through
  • Will your reviewer review them closely?
    • If so... it tends to be a waste of her time
    • But if not... occasionally mistakes will slip through
  • Bad signal-to-noise in your codebase
    • Especially for a series of nested classes
  • Your class will evolve; what a pain!
    • This is when mistakes usually happen (next slide)

10 dari 29

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.)

11 dari 29

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:

  • Bugs caused by not bothering to do all that in your value types
  • Avoidance of even creating new types in the first place

12 dari 29

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.

13 dari 29

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.

14 dari 29

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

}

15 dari 29

Solution 3: problems

  • You've got to stop somewhere with these things. Which N is big enough? 10? 20?
  • Primitive boxing overhead
  • Your choice to use this solution is API-visible.
    • Whatever library contains these base classes must become a runtime dependency for everyone using your types
    • Users can depend on the fact that your type extends TupleN
    • But if you delegate to a tuple class instead, it costs memory

16 dari 29

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;

}

}

17 dari 29

Costs of the reflective base class

  • Reflection is slower than regular code
    • caching helps, but then...
  • Memory usage of a typical 4-field object with cached hash code:
    • coded by hand → 32 bytes
    • using this → 88 to 154 bytes!*
  • Leaks onto your API, adding a runtime dependency, which is bad as explained in the previous item

18 dari 29

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....

19 dari 29

Solution 5: Protocol Buffers

This use case isn't a design goal for protocol buffers.

  • Creates structs, not classes (slide 3)
    • want: fields of any data types? implement an interface (e.g. Comparable)? add custom operations? encapsulate your state? use murmur for hashCode? ignore some fields in equals? add useful javadoc? etc. → answer: NO!
    • a few of those could theoretically be improved in proto itself... but this just isn't proto's core use case so it won't happen
  • Choice to use it is API-visible, gets baked in, as before
  • Generated code is large, with large API
  • Runtime dependency

20 dari 29

Solution 6: Codegen from DSL

Configure fields in our special language, then generate Java code from that -- a very conventional kind of codegen.

  • Pros:
    • Hand-written code can be as terse as we like
  • Cons:
    • The same limited capabilities as Solution 5, unless we dial up the complexity a lot.
    • It's yet another mini-language. The world has millions of these.

21 dari 29

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.

  • Pros:
    • Maximally concise value classes.
  • Cons:
    • The inserted code is invisible. This makes for a poor experience with tools, such as debuggers and code explorers.
    • The compiler hacks are non-standard and fragile.
    • Lombok functions outside the boundaries of how things work in Java. It is extralinguistic. This is a dangerous line to cross that is sure to cause confusion.

22 dari 29

Introducing AutoValue

Our new solution!

  • You write an abstract class
  • It has abstract accessors, but no fields
  • Annotate it with @AutoValue
  • Javac generates a concrete subclass for you
  • Callers only ever see the parent type

23 dari 29

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

}

24 dari 29

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...

}

25 dari 29

Advantages, part 1

  • User writes only plain old Java code
  • No runtime impact
    • no dependency (@AutoValue has source retention)
    • performs comparably to hand-written code�(1-morphic, so accessors are still inlinable)
  • Virtually no impact on API
    • Exception: if you already committed to a public constructor, you can't switch to this
  • No magical modifying of existing classes
  • Still just a single javac pass to compile!

26 dari 29

Plus all the flexibility of code-by-hand:

  • Accessors can be public but don't have to be
  • You can have other operations
  • You can implement other interfaces
  • You can hand-write hashCode etc. if you want
    • the generator will see this and won't generate one
  • You can have multiple static factories
  • Do you have "derived" fields, which eq/hc can ignore? Just put them in your base class.
  • You can use @Nullable on accessors
    • if present, it will generate null-safe equals etc.
    • if not, it will checkNotNull on construction

27 dari 29

Disadvantages

  • Bootstrap can be annoying—when you first type new AutoValue_Foo it may go red in the IDE
  • Some once-per-project build setup required
  • Not great for mutable field types (no defensive copy on the way out)
  • and...

28 dari 29

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?)

29 dari 29

AutoValue users’ guide