How to write less and better code, or Project Lombok
I have long intended to write about Project Lombok, so much so that I am probably doing it when every self-respecting Java developer has already heard about it. Nevertheless, it is worth mentioning, if only to remind myself that one should not hesitate to try performance-enhancing tools and see if they fit, and Lombok is certainly enhancing performance of a Java-coder by allowing to simultaneously write less code and add to its quality, which is no small matter.
What is it that Java opponents usually say about its weaknesses?
Java is too verbose.
(c) Every Java opponent
Unfortunately, there’s a lot of truth in this statement. Imagine a simple data class you want to have to store the personal information – name, age etc. It might look like this.
public class PersonSimple { private String lastName; private String firstName; private Integer age; public String getLastName() { return lastName; } public String getFirstName() { return firstName; } public Integer getAge() { return age; } public void setLastName(String lastName) { this.lastName = lastName; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setAge(Integer age) { this.age = age; } }
OK, you say. I generated all this stuff with the IDE, wasn’t that hard. But we also need a hashCode() and equals(). Because you might want to keep the instances in collections and check equality. No problem, most IDEs will allow you to generate these as well as getters and setters. And they will throw in a toString() generator to help you output the objects and see what’s in them.
public class PersonSimple { private String lastName; private String firstName; private Integer age; public String getLastName() { return lastName; } public String getFirstName() { return firstName; } public Integer getAge() { return age; } public void setLastName(String lastName) { this.lastName = lastName; } public void setFirstName(String firstName) { this.firstName = firstName; } public void setAge(Integer age) { this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PersonSimple that = (PersonSimple) o; return Objects.equals(lastName, that.lastName) && Objects.equals(firstName, that.firstName) && Objects.equals(age, that.age); } @Override public int hashCode() { return Objects.hash(lastName, firstName, age); } @Override public String toString() { return "PersonSimple{" + "lastName='" + lastName + '\'' + ", firstName='" + firstName + '\'' + ", age=" + age + '}'; } }
OK then. All this stuff was generated by IntelliJ IDEA. It is not that difficult right? Well no. But now you are thinking of Josh Bloch and decide to apply a Builder pattern. This time, you have to do a little manual work. What you will likely have in the end will be something close to this.
public class PersonSimple { private final String lastName; private final String firstName; private final Integer age; private PersonSimple(String lastName, String firstName, Integer age) { this.lastName = lastName; this.firstName = firstName; this.age = age; } public String getLastName() { return lastName; } public String getFirstName() { return firstName; } public Integer getAge() { return age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PersonSimple that = (PersonSimple) o; return Objects.equals(lastName, that.lastName) && Objects.equals(firstName, that.firstName) && Objects.equals(age, that.age); } @Override public int hashCode() { return Objects.hash(lastName, firstName, age); } @Override public String toString() { return "PersonSimple{" + "lastName='" + lastName + '\'' + ", firstName='" + firstName + '\'' + ", age=" + age + '}'; } public static class Builder { private String lastName; private String firstName; private Integer age; public Builder setLastName(String lastName) { this.lastName = lastName; return this; } public Builder setFirstName(String firstName) { this.firstName = firstName; return this; } public Builder setAge(Integer age) { this.age = age; return this; } public PersonSimple build() { return new PersonSimple(lastName, firstName, age); } } }
So. We have a builder, and now our PersonSimple can be created with a piece of code like this.
final PersonSimple john = new Person.Builder() .setFirstName("John") .setLastName("Doe") .setAge(30) .build(); System.out.println(john);
But you had to create so many things. You have:
- a data class with an all-arguments private constructor;
- three getters on a data class;
- an accompanying builder class with three setters;
- a build() method on a builder class that calls the private data class constructor;
- and don’t forget the hashCode(), equals() and toString() methods, though generated.
The code now takes more than 70 lines. And every time you need a new field, you will have to take care of it in at least three places – getter in the data class, setter in the builder class, and the constructor.
What if I were to show you how to do the same things with Project Lombok?
OK, here goes.
@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; }
- We generated the builder class with @Builder annotation. toBuilder=true means that we additionally created a toBuilder() instance method that creates a new instance of the builder class, initialized with the values from the current instance.
- We added a toString() method with a @ToString annotation.
- We added hashCode() and equals() with, you guessed it, @EqualsAndHashCode.
- We made the all-argument constructor private with @AllArgsConstructor(access = AccessLevel.PRIVATE).
- We added standard getters to the class fields with @Getter annotations.
It is now fifteen lines of code. Fifteen! We just decreased the code five times. The gain would be even better for a class with a lot of fields.
So, what exactly does Project Lombok do? It generates all the boilerplate during compile time, allowing you to avoid writing that code manually or generating it with an IDE. It saves you a lot of time and allows to create prettier code with less effort.
After the lombokization of your code, the person can be created like this.
private static Person JOHN = Person.builder() .firstName("John") .lastName("Doe") .age(30) .build();
To add Lombok to your project, you need to add a dependency for it and also in my case install a Lombok plugin for IDEA. The Gradle configuration is described here and maven here.
All the features of Lombok are described here. Please have a look and see if there’s anything else that might come in useful, because what I described here is just a small part of what it has.
The code from examples is stored in my github repository.
I wish you clean and concise code!
Published on Java Code Geeks with permission by Maryna Cherniavska, partner at our JCG program. See the original article here: How to write less and better code, or Project Lombok Opinions expressed by Java Code Geeks contributors are their own. |
Hmmm, I agree Lombok is a great tool, but I’ve always strongly disagreed using it in a large real life project… Here is why: * We’re already heavily relying on bytecode manipulation tools like aspectj with Spring and friends in the Java world. More bytecode manipulation tends to make the application behavior less deterministic when live in production. It’s also true in tests contexts where we use instrumentation like cobertura etc. * Lombok-enabled classes/jars can’t be used easily anymore by third parties. Ex: if you happen to debug step by step a code that uses lombok, one must use a… Read more »
Just one small note, I didn’t generate the setters, just the getters. And you are absolutely right, code should be good even in the code samples, because one never knows how it’ll be used. I might have been a bit careless, because I was just exploring the possibilities. The reasons you gave are really well based and well explained. I can certainly understand that and agree that this tool is not for every system. It still can be useful for some which don’t have the same concerns. I really am thankful to you for raising these questions! This illustrates the… Read more »
Or just use kotlin ;)
Agreed. If productivity on the jvm is your first goal, use kotlin or groovy or whatever :)
We are looking into it for the future. But we have existing projects that won’t be converted or completely redone. Java is still far from being obsolete and Kotlin is still quite new.