1. Overview
AutoValue is a source code generator for Java, and more specifically it’s a library for generating source code for value objects or value-typed objects.
In order to generate a value-type object all you have to do is to annotate an abstract class with the @AutoValue annotation and compile your class. What is generated is a value object with accessor methods, parameterized constructor, properly overridden toString(), equals(Object) and hashCode() methods.
The following code snippet is a quick example of an abstract class that when compiled will result in a value object named 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(); }
Let’s continue and find out more about value objects, why we need them and how AutoValue can help make the task of generating and refactoring code much less time consuming.
2. Maven Setup
To use AutoValue in a Maven projects, you need to include the following dependency in the pom.xml:
<dependency> <groupId>com.google.auto.value</groupId> <artifactId>auto-value</artifactId> <version>1.2</version> </dependency>
The latest version can be found by following this link.
3. Value-Typed Objects
Value-types are the end product of library, so to appreciate its place in our development tasks, we must thoroughly understand value-types, what they are, what they are not and why we need them.
3.1. What are Value-Types?
Value-type objects are objects whose equality to one another is not determined by identity but rather their internal state. This means that two instances of a value-typed object are considered equal as long as they have equal field values.
Typically, value-types are immutable. Their fields must be made final and they must not have setter methods as this will make them changeable after instantiation.
They must consume all field values through a constructor or a factory method.
Value-types are not JavaBeans because they don’t have a default or zero argument constructor and neither do they have setter methods, similarly, they are not Data Transfer Objects nor Plain Old Java Objects.
Additionally, a value-typed class must be final, so that they are not extendable, least that someone overrides the methods. JavaBeans, DTOs and POJOs need not be final.
3.2. Creating a Value-Type
Assuming we want to create a value-type called Foo with fields called text and number. How would we go about it?
We would make a final class and mark all its fields as final. Then we would use the IDE to generate the constructor, the hashCode() method, the equals(Object) method, the getters as mandatory methods and a toString() method, and we would have a class like so:
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; } }
After creating an instance of Foo, we expect it’s internal state to remain the same for its entire life cycle.
As we will see in a following subsection the hashCode of an object must change from instance to instance, but for value-types, we have to tie it to the fields which define the internal state of the value object.
Therefore, even changing a field of the same object would change the hashCode value.
3.3. How Value-Types Work
The reason value-types must be immutable is to prevent any change to their internal state by the application after they have been instantiated.
Whenever we want to compare any two value-typed objects, we must therefore use the equals(Object) method of the Object class.
This means that we must always override this method in our own value-types and only return true if the fields of the value objects we are comparing have equal values.
Moreover, for us to use our value objects in hash-based collections like HashSets and HashMaps without breaking, we must properly implement the hashCode() method.
3.4. Why we Need Value-Types
The need for value-types comes up quite often. These are cases where we would like to override the default behaviour of the original Object class.
As we already know, the default implementation of the Object class considers two objects equal when they have the same identity however for our purposes we consider two objects equal when they have the same internal state.
Assuming we would like to create a money object as follows:
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 }
We can run the following test on it to test it’s equality:
@Test public void givenTwoSameValueMoneyObjects_whenEqualityTestFails_thenCorrect() { MutableMoney m1 = new MutableMoney(10000, "USD"); MutableMoney m2 = new MutableMoney(10000, "USD"); assertFalse(m1.equals(m2)); }
Notice the semantics of the test.
We consider it to have passed when the two money objects are not equal. This is because we have not overridden the equals method so equality is measured by comparing the memory references of the objects, which of course are not going to be different, because they are different objects occupying different memory locations.
Each object represents 10,000 USD but Java tells us our money objects are not equal. We want the two objects to test unequal only when either the currency amounts are different or the currency types are different.
Now let us create an equivalent value object and this time we will let the IDE generate most of the code:
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; } }
The only difference is that we overrode the equals(Object) and hashCode() methods, now we have control over how we want Java to compare our money objects. Let’s run its equivalent test:
@Test public void givenTwoSameValueMoneyValueObjects_whenEqualityTestPasses_thenCorrect() { ImmutableMoney m1 = new ImmutableMoney(10000, "USD"); ImmutableMoney m2 = new ImmutableMoney(10000, "USD"); assertTrue(m1.equals(m2)); }
Notice the semantics of this test, we expect it to pass when both money objects test equal via the equals method.
4. Why AutoValue?
Now that we thoroughly understand value-types and why we need them, we can look at AutoValue and how it comes into the equation.
4.1. Issues With Hand-Coding
When we create value-types like we have done in the preceding section, we will run into a number of issues related with bad design and a lot of boilerplate code.
A two field class will have 9 lines of code: one for package declaration, two for the class signature and its closing brace, two for field declarations, two for constructors and its closing brace and two for initializing the fields, but then we need getters for the fields, each taking three more lines of code, making six extra lines.
Overriding the hashCode() and equalTo(Object) methods require about 9 lines and 18 lines respectively and overriding the toString() method adds another five lines.
That means a well-formatted code base for our two field class would take about 50 lines of code.
4.2 IDEs to The Rescue?
This is is easy with an IDE like Eclipse or IntilliJ and with only one or two value-typed classes to create. Think about a multitude of such classes to create, would it still be as easy even if the IDE helps us?
Fast forward, some months down the road, assume we have to revisit our code and make amendments to our Money classes and perhaps convert the currency field from the String type to another value-type called Currency.
4.3 IDEs not Really so Helpful
An IDE like Eclipse can’t simply edit for us our accessor methods nor the toString(), hashCode() or equals(Object) methods.
This refactoring would have to be done by hand. Editing code increases the potential for bugs and with every new field we add to the Money class, the number of lines increases exponentially.
Recognizing the fact that this scenario happens, that it happens often and in large volumes will make us really appreciate the role of AutoValue.
5. AutoValue Example
The problem AutoValue solves is to take all the boilerplate code that we talked about in the preceding section, out of our way so that we never have to write it, edit it or even read it.
We will look at the very same Money example, but this time with AutoValue. We will call this class AutoValueMoney for the sake of consistency:
@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); } }
What has happened is that we write an abstract class, define abstract accessors for it but no fields, we annotate the class with @AutoValue all totalling to only 8 lines of code, and javac generates a concrete subclass for us which looks like this:
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; } }
We never have to deal with this class directly at all, neither do we have to edit it when we need to add more fields or make changes to our fields like the currency scenario in the previous section.
Javac will always regenerate updated code for us.
While using this new value-type, all callers see is only the parent type as we will see in the following unit tests.
Here is a test that verifies that our fields are being set correctly:
@Test public void givenValueTypeWithAutoValue_whenFieldsCorrectlySet_thenCorrect() { AutoValueMoney m = AutoValueMoney.create("USD", 10000); assertEquals(m.getAmount(), 10000); assertEquals(m.getCurrency(), "USD"); }
A test to verify that two AutoValueMoney objects with the same currency and same amount test equal follows:
@Test public void given2EqualValueTypesWithAutoValue_whenEqual_thenCorrect() { AutoValueMoney m1 = AutoValueMoney.create("USD", 5000); AutoValueMoney m2 = AutoValueMoney.create("USD", 5000); assertTrue(m1.equals(m2)); }
When we change the currency type of one money object to GBP, the test: 5000 GBP == 5000 USD is no longer true:
@Test public void given2DifferentValueTypesWithAutoValue_whenNotEqual_thenCorrect() { AutoValueMoney m1 = AutoValueMoney.create("GBP", 5000); AutoValueMoney m2 = AutoValueMoney.create("USD", 5000); assertFalse(m1.equals(m2)); }
6. AutoValue With Builders
The initial example we have looked at covers the basic usage of AutoValue using a static factory method as our public creation API.
Notice that if all our fields were Strings, it would be easy to interchange them as we passed them to the static factory method, like placing amount in the place of currency and vice versa.
This is especially likely to happen if we have many fields and all are of String type. This problem is made worse by the fact that with AutoValue, all fields are initialized through the constructor.
To solve this problem we should use the builder pattern. Fortunately. this can be generated by AutoValue.
Our AutoValue class does not really change much, except that the static factory method is replaced by a builder:
@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(); } }
The generated class is just the same as the first one but a concrete inner class for the builder is generated as well implementing the abstract methods in the builder:
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); } }
Notice also how the test results don’t change.
If we want to know that the field values are actually correctly set through the builder, we can execute this test:
@Test public void givenValueTypeWithBuilder_whenFieldsCorrectlySet_thenCorrect() { AutoValueMoneyWithBuilder m = AutoValueMoneyWithBuilder.builder(). setAmount(5000).setCurrency("USD").build(); assertEquals(m.getAmount(), 5000); assertEquals(m.getCurrency(), "USD"); }
To test that equality depends on internal state:
@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)); }
And when the field values are different:
@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)); }
7. Conclusion
In this tutorial, we have introduced most of the basics of Google’s AutoValue library and how to use it to create value-type’s with very little code on our part.
An alternative to Google’s AutoValue is the Lombok project – you can have a look at the introductory article about using Lombok here.
The full implementation of all these examples and code snippets can be found in the AutoValue github project.