Lombok, AutoValue and Immutables, or How to write less and better code returns
In the previous post about Lombok library, I have described a library that helps to deal with boilerplate code in Java (and yes I know that these problems are already solved in Kotlin, but this is real life and we can’t just all sit and rewrite every existing project once a newer or a simpler language appears). But as many things in life, project Lombok has its alternatives. Let’s give them a chance as well.
Code samples for this article can be found here and here.
Google AutoValue
It is really an alternative to Lombok – because you can’t use both at once. Or, at least it turns out that you will have difficulties while using both in the same project with IntelliJ IDEA, which is the IDE of choice for many and yours truly – because the two libraries deal with the annotation processing differently. So, neither can live while the other survives, which is approximately how a prophecy for Harry Potter and Voldemort sounded.
So, we already know how the Person class looked with Lombok annotations:
@Builder(toBuilder = true) @ToString @EqualsAndHashCode @AllArgsConstructor(access = AccessLevel.PRIVATE) public class Person { @NonNull @Getter private final String lastName; @NonNull @Getter private final String firstName; @NonNull @Getter private final Integer age; }
If we create a new project and make it use autovalue as described here, we can imitate pretty much the same model with AutoValue Builders.
Now let’s see how the AutoValue model looks:
package autovalue.model; import com.google.auto.value.AutoValue; @AutoValue public abstract class Person { public abstract String lastName(); public abstract String firstName(); public abstract Integer age(); public static Person create(String lastName, String firstName, Integer age) { return builder().lastName(lastName).firstName(firstName).age(age).build(); } public static Builder builder() { return new AutoValue_Person.Builder(); } @AutoValue.Builder public abstract static class Builder { public abstract Builder lastName(String lastName); public abstract Builder firstName(String firstName); public abstract Builder age(Integer age); public abstract Person build(); } }
What you can see is, there’s definitely more code.
While Lombok generates a builder with a single annotation, AutoValue will make you create your own builder code – not all of it though. Basically you define your interfaces and the implementation is left to AutoValue generated code, you don’t have to actually implement the code that is in getters and setters. Even if we agree that the AutoValue getter interfaces won’t take much more time or space than the Lombok field definitions, for some people it might still be a hassle and an annoyance to write the AutoValue builder code.
However, it allows for greater flexibility, because you can actually change the builder method names. Also, a big win is code analysis and usage search – this way, you can actually look for usages of actual getters and setters separately, which might also be important for developers.
The instance is created in the same way as with Lombok.
final Person anna = Person.builder() .age(31) .firstName("Anna") .lastName("Smith") .build();
All our tests run with minimal code changes, mostly because AutoValue doesn’t have a way to transform an instance to a builder (or at least I could not easily find it), so copying is just calling a static factory method:
package autovalue.model; import org.junit.Test; import static org.assertj.core.api.Java6Assertions.assertThat; public class PersonTest { private static Person JOHN = Person.builder() .firstName("John") .lastName("Doe") .age(30) .build(); private static Person JANE = Person.builder() .firstName("Jane") .lastName("Doe") .age(30) .build(); @Test public void testEquals() throws Exception { Person JOHN_COPY = Person.create(JOHN.lastName(), JOHN.firstName(), JOHN.age()); assertThat(JOHN_COPY).isEqualTo(JOHN); } @Test public void testNotEquals() throws Exception { assertThat(JANE).isNotEqualTo(JOHN); } @Test public void testHashCode() throws Exception { Person JOHN_COPY = Person.create(JOHN.lastName(), JOHN.firstName(), JOHN.age()); assertThat(JOHN_COPY.hashCode()).isEqualTo(JOHN.hashCode()); } @Test public void testHashCodeNotEquals() throws Exception { Person JOHN_COPY = Person.create(JOHN.lastName(), JOHN.firstName(), JOHN.age()); assertThat(JOHN_COPY.hashCode()).isNotEqualTo(JANE.hashCode()); } @Test public void testToString() throws Exception { String jane = JANE.toString(); assertThat(jane).contains(JANE.lastName()); assertThat(jane).contains(JANE.firstName()); assertThat(jane).contains("" + JANE.age()); assertThat(jane).doesNotContain(JOHN.firstName()); } }
Other differences that are immediately obvious:
- AutoValue classes written by you are always abstract. They are implemented in AutoValue generated code.
- AutoValue classes are automatically immutable. There’s a workaround for having them have properties of immutable types. Even if you explicitly wanted to have setters on your instances, you can’t.
Why should you use AutoValue? The AutoValue creators took care to describe the gains of the library here and even create a whole presentation about it.
Immutables library
The library also uses Java annotation processors to generate simple, safe and consistent value objects. Well, same as the previous two. What else is new? Let’s see.
The simplest value class would look like this.
package immutables.model; import org.immutables.value.Value; @Value.Immutable public abstract class Person { public abstract String lastName(); public abstract String firstName(); public abstract Integer age(); }
So, there’s the same principle of having abstract classes, that are only implemented in the generated code. For that, you need to enable the IDE annotation processors, same as you do for Lombok (but not for AutoValue, as there it is done by a gradle plugin).
How does the object creation look, then?
final Person anna = ImmutablePerson.builder() .age(31) .firstName("Anna") .lastName("Smith") .build(); System.out.println(anna);
The most obvious differences are, at first glance:
- We don’t declare the builder methods.
- The static builder/factory methods are created not on our own class, but on the generated one.
- Same as AutoValue, there’s no way to generate setters on the class, just on the builder.
- The generated class also automatically adds with-ers, that is, instance methods, that allow to create a copy of the instance by changing one property:
final ImmutablePerson anna = ImmutablePerson.builder() .age(31) .firstName("Anna") .lastName("Smith") .build(); System.out.println(anna); final ImmutablePerson annaTheSecond = anna.withAge(23).withLastName("Smurf"); System.out.println(annaTheSecond);
- The builder has an automatically added from() method, that allows to create an exact copy of the instance, and there’s also a generated static copyOf() method on the generated class:
Person JOHN_COPY = ImmutablePerson.builder().from(JOHN).build(); // OR Person JOHN_COPY = ImmutablePerson.copyOf(JOHN);
And again, our test runs with minimal changes, which are mainly about how we copy the instances:
package immutables.model; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public class PersonTest { private static Person JOHN = ImmutablePerson.builder() .firstName("John") .lastName("Doe") .age(30) .build(); private static Person JANE = ImmutablePerson.builder() .firstName("Jane") .lastName("Doe") .age(30) .build(); @Test public void testEquals() throws Exception { //ImmutablePerson JOHN_COPY = ImmutablePerson.builder().from(JOHN).build(); Person JOHN_COPY = ImmutablePerson.copyOf(JOHN); assertThat(JOHN_COPY).isEqualTo(JOHN); } @Test public void testNotEquals() throws Exception { assertThat(JANE).isNotEqualTo(JOHN); } @Test public void testHashCode() throws Exception { Person JOHN_COPY = ImmutablePerson.copyOf(JOHN); assertThat(JOHN_COPY.hashCode()).isEqualTo(JOHN.hashCode()); } @Test public void testHashCodeNotEquals() throws Exception { Person JOHN_COPY = ImmutablePerson.copyOf(JOHN); assertThat(JOHN_COPY.hashCode()).isNotEqualTo(JANE.hashCode()); } @Test public void testToString() throws Exception { String jane = JANE.toString(); assertThat(jane).contains(JANE.firstName()); assertThat(jane).contains(JANE.lastName()); assertThat(jane).contains("" + JANE.age()); assertThat(jane).doesNotContain(JOHN.firstName()); } }
There’s much more to say about Immutables library, so there’s a pretty big manual for it here. Here in this article we only scratched the surface a little. There’s for example much more details about JSON serialization with Immitables and style customizations (method prefixes, builder names etc.) and even repository generation for Mongo so that documents could be treated as immutables. But that is all much more than I care for to touch in this simple article.
The takeaway is, one of the challenges of the not-going-anywhere-yet Java language is verbosity and boilerplate code. But there’s numerous tools to deal with it, and one can choose a library that fits best, instead of coding by copy-paste or trying to write your own code generator.
Use them well.
Use it well.
Published on Java Code Geeks with permission by Maryna Cherniavska, partner at our JCG program. See the original article here: Lombok, AutoValue and Immutables, or How to write less and better code returns Opinions expressed by Java Code Geeks contributors are their own. |
I’m happy tout use Groovy for many years :-)