Lombok: Towards beautiful Java code

def gcd(x: Long, y: Long): Long =
  if (y == 0) x else gcd(y, x % y)

Can you tell me what this code does?

data class User(val name: String, val age: Int)

What about this?

The first is a Scala implementation of Euclid's algorithm for calculating the greatest common divisor. It says that if y is 0, the GCD of x and y is x, otherwise it's the GCD of y and x mod y.

The Scala code is essentially a direct translation of the algorithm, and even if you don't know Scala you can more or less figure it out if you know of Euclid's algorithm.

The second is a data class in Kotlin. This is a class whose main purpose is to hold data, in this case to hold a String value for name and a Int value for age.

This is again pretty clear even if you know very little Kotlin. The classes behave as you'd expect them to - you can print them out with a decent standard representation, compare instances for equality, and hash them in a reasonable way.

Now. How about this?

    public class User {
	private final String name;
	private final Integer age;

	private User(String name, Integer age) {
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public String getAge() {
		return age;
	}

	public static Builder builder() {
		return new Builder();
	}

	private static class Builder {
		private String name;
		private Integer age;

		public Builder name(String name) {
			this.name = name;
			return this;
		}

		public Builder age(Integer age) {
			this.age = age;
			return this;
		}

		public User build() {
			return new User(this.name, this.age);
		}
	}

	@Override
	public int hashCode() {
		return Objects.hash(name, age);
	}

	@Override
	public boolean equals(Object o) {
		if (this == 0) {
			return true;
		}
		if (o == null || getClass() != o.getClass()) {
			return false;
		}
		User user = (User) o;
		return Objects.equals(name, user.name)
			&& Objects.equals(age, user.age)
	}

	@Override
	public String toString() {
		return "User{" +
			   "name='" + name + "'" +
			   ", age=" + age.toString() +
			   "}";
	}
}

That's 66 lines of Java to create a value object which holds two variables. In functionality this isn't a million miles away from the Kotlin example earlier in this post (the major difference is the inclusion of a Builder, which achieves similar functionality to Kotlin's named arguments).

In readability it's a million miles away from the Kotlin example. For just two instance variables we have this huge load of code that makes our job of understanding the class massively more difficult.

Worse still, in 99% of cases - the code for the getters, builder, hashCode, equals, and toString are completely standard, so much so that many developers use their IDEs to generate them.


The solution I'd like to propose is the use of Project Lombok, an annotation-based code generation tool that has been around since 2009 and supports Java 6-8 (with support for 9 coming soon)

It comes with a bunch of annotations that will generate boilerplate at compile-time - avoiding the need for it to sit around in your codebase.

ToString

@ToString
public class User {

This generates a valid ToString method that is equivalent to the one we wrote manually.

Equals and Hashcode

@EqualsAndHashCode
public class User {

This generates the equals and hashCode methods. By default this will include all instance variables in the class but arguments can specify variables to include/exclude.

Getters

We can generate getters for each individual field by adding the @Getter annotation.

    @Getter
    private final String name;
    @Getter
    private final Integer age;

Or create a getter for all instance variables by applying the @Getter annotation to the class directly.

    @Getter
    public class User {

You can of course disable the getter on individual instance variables, by applying the @Getter(AccessLevel.NONE) annotation.

This leaves us with three annotations on the class, @Getter, @EqualsAndHashCode, and @ToString. We can simplify further by replacing these three with the single annotation @Value.

    @Value
    public class User {

With this single annotation we're generating valid and consistent equals, hashCode, and toString methods, and a getter for each instance variable.

Builder

Builders are a very common pattern in Java development. Not so useful in this case because there are only two variables but as your data or value objects grow they become invaluable to allow for optional constructor parameters.

However they're very repetitive and suffer from the issues mentioned earlier in this post. Adding an instance variable means the variable also needs to be added to the builder, as does a setter, and it needs to be taken into account when the builder constructs the instance.

Of course, the solution to this is the same as always.

    @Builder
    public class User {

Just add another annotation to the class. Our code is now a lot simpler than it was at the start.

    @Builder
    @Value
    public class User {
        private final String name;
        private final Integer age;
    }

These two annotations cut 60 lines in this tiny class. Across real codebases with much larger data and value objects it can cut thousands of lines.

These cut lines are now code that doesn't need to be tested, or kept up to date, and it's very unlikely to introduce bugs as it's always generated the same. Most importantly we can now look at the class and see immediately what it does without having to parse huge amounts of boilerplate.

Other Annotations

There are of course other annotations in Lombok. In my opinion the most useful are @Setter, which generates setters, and @Data which is the mutable equivalent of @Value.

NoArgsConstructor, RequiredArgsConstructor and AllArgsConstructor come in handy on occasion for the same reasons I've outlined: they avoid you writing, testing, maintaining, and reading needless boilerplate.

@Log can also be useful to remove that copy and pasted line at the top of most Java classes setting up logging.

There are a number of other annotations that I believe are less useful or more experimental. You can see them all on the Lombok website.

How it works
Lombok uses JSR 269, which allows for pluggable annotation processors at compile-time. However, the JSR doesn't allow for modification of classes - only for generating new ones. So Lombok also used some internal APIs to achieve its goals.

This is the bit most people don't like about Lombok. On the surface it can seem quite hacky, and maybe a bit fragile. I'm sure there are a range of opinions on using internal APIs and I'd be happy to discuss it with you.

Lombok runs at compile time - which means the jar that is generated contains no evidence that Lombok was used. The generated methods behave exactly as if they were handwritten, and the Lombok library doesn't need to be included past compile time.

It also doesn't use reflection for the generated methods. The generated code follows the normal patterns that your IDE does, or that you would when writing by hand.

Issues

The first issue to mention is that by default, your IDE won't be able to see the generated methods until you've run the compiler that actually creates them, so it'll give you squiggly red lines when you try to use them.

There are plugins for Eclipse and IntelliJ that allow the IDEs to see the generated methods before running the compiler, but if you use something else you're out of luck.

Even with these plugins, it can still be difficult to refactor - if you want to change the name of an instance variable and the associated getter for example. The best way to deal with this is to manually write a getter - refactor that to the new name - and then remove the new getter allowing Lombok to take over. It's not the end of the world but it's not ideal.

Also, because the methods don't exist in source - this may cause issues for any static analysis you run against your source. This can be solved, through the use of the delombok tool, which I'll talk about later in this post, but it is something to be aware of.

And finally, because of the use of internal APIs, Jigsaw in Java 9 will just block Lombok from working by default. I last saw an update on the Lombok project a few months ago where they were making good progress with Java 9 compatibility, but of course until it's working completely we can't say for certain.

Delombok

Delombok is a command which will generate the Java code for the builders, getters, setters, etc. directly inside the Java source file.

This can be used to generate the code before you run static analysis, and it's also the security in case Lombok is no longer viable at some point. One command will regenerate all of that code that was deleted.

Alternatives

IDE Generation

The most obvious alternative solution is let your IDE do it. This is what most of us are doing already. Most IDEs have getters and setters, hashCode, equals, and toString built in - and you can get plugins to generate builders.

However, as I've mentioned previously, it makes the code harder to figure out due to all the boilerplate, and it's easy to forget to re-generate something after a change.

AutoValue/Immutables

AutoValue and Immutables are two very similar projects, specifically for reducing the boilerplate around value objects.

They both use JSR 269 without the use of internal APIs, which means we can be a bit more confident that they won't stop working with a future version of the JDK. But I personally found the code that you need to write to use these projects a lot less pleasing than that which you write when using Lombok.

You create abstract classes or interfaces that define the getters, then the library generates a class implementing that interface, complete with builders etc. However you then end up with these weird class names like AutoValue_Animal, which I wouldn't say I like less than getters, but I'm not a huge fan of.


There are other solutions such as moving to another language, or just learning to love the boilerplate. But within the projects I work on we've adopted Lombok and are very happy with the subsequent improvement in the readability of our code. I'm interested in hearing other people's experiences using Lombok or other techniques to deal with Java boilerplate.

This post was originally delivered as an internal technical talk on 19/10/2017.

Show Comments

Get the latest posts delivered right to your inbox.