The Project Lombok library provides additional Java annotations that make your code less verbose. One of the main criticisms of the Java language has always been that it requires so much boilerplate code — that is, many repeated sections of code with minimal variation. This criticism isn't entirely unfair. However, Project Lombok provides a convenient solution by reducing some of the most common boilerplate code that clogs our codebases.
The upshot is this: a concise codebase takes less time to write, is much easier to read, and is less prone to bugs. The speed at which we can write our code has accelerated significantly with modern IDEs (check out Generate Code in IntelliJ if you haven't already). But the one thing that code generation shortcuts cannot do is make our code dramatically shorter. Enter Project Lombok.
Installing Project Lombok
Installing Project Lombok is as simple as adding it as a Maven or Gradle dependency.
For Maven, you simply need to open your pom.xml file and paste the dependency between the <dependencies> tags. The resulting section should look something like this:
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
</dependencies>
If you are using Gradle as your build tool, you can simply add the Lombok dependency to your build.gradle file. You have two options when adding the dependency, depending on whether you are using Groovy or Kotlin to write your build scripts. You don't have to understand the difference to use Lombok, just note that there is a tiny variation in the syntax.
Using Groovy, it should look like this:
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.20'
annotationProcessor 'org.projectlombok:lombok:1.18.20'
testCompileOnly 'org.projectlombok:lombok:1.18.20'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.20'
}
Or just use a gradle plugin which does the same:
plugins {
id "io.freefair.lombok" version "6.0.0-m2"
}
Using Kotlin, it is almost the same:
dependencies {
compileOnly("org.projectlombok:lombok:1.18.20")
annotationProcessor("org.projectlombok:lombok:1.18.20")
testCompileOnly("org.projectlombok:lombok:1.18.20")
testAnnotationProcessor("org.projectlombok:lombok:1.18.20")
}
Also, see an alternative way:
plugins {
id("io.freefair.lombok") version "6.0.0-m2"
}
The next step is integrating Lombok with IDE. IntelliJ IDEA has short instructions on Project Lombok's setup webpage. The latest versions of IntelliJ no longer require a plugin to use Lombok.
@Data — the one annotation to rule them all
Before diving into the theory behind Lombok, let's take a look at a direct comparison of a class written the standard Java way, followed by the same class written with Lombok annotations. We have a Dog class that has four fields: name, breed, age, and color. We will create default and all-arguments constructors, getters and setters for each field, a readable toString() method, and custom hashCode() and equals() methods.
public class Dog {
String name;
String breed;
int age;
String color;
public Dog(String name, String breed, int age, String color) {
this.name = name;
this.breed = breed;
this.age = age;
this.color = color;
}
public Dog() {}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBreed() {
return breed;
}
public void setBreed(String breed) {
this.breed = breed;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", breed='" + breed + '\'' +
", age=" + age +
", color='" + color + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Dog dog = (Dog) o;
if (age != dog.age) return false;
if (!name.equals(dog.name)) return false;
if (!breed.equals(dog.breed)) return false;
if (!color.equals(dog.color)) return false;
return true;
}
@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + breed.hashCode();
result = 31 * result + age;
result = 31 * result + color.hashCode();
return result;
}
}
No matter how you slice it, that wall of code is a big hairy mess. Let's rewrite it using just 3 annotations from Project Lombok.
The @Data annotation does most of the work, but we can throw in @NoArgsConstructor and @AllArgsConstructor to give us a couple of extra constructors to work with just in case we need them:
@Data
@AllArgsConstructor
@NoArgsConstructor
class Dog {
String name;
String breed;
int age;
String color;
}
Much better, right?
Under the hood
So how does Lombok accomplish this near-miraculous reduction in code size? Well, it's simpler than you might think.
We already (hopefully) know that IDEs can generate all of that boilerplate code for us, so long as we provide the class declaration and its fields. But instead of immediately writing the boilerplate code, it is as if the annotations signal the IDE to wait until compile time to generate them. This way, we have all of the getters, setters, hashCode, equals, and toString methods when we need them, but we don't have to write them or even look at them beforehand. This is the power of Lombok annotations.
Digging a little deeper, the Lombok annotation processor actually reads the annotations and precompiles the bytecode for them. Java has three different retention policies for annotations, which determine how long the annotations will be kept in the code before potentially being discarded. Lombok annotations have a retention policy of RetentionPolicy.Source , which means that the annotations only need to be present in the source code. Once the source code is compiled, the .class files will already contain all of the information Lombok generated for us, therefore the annotations are now redundant.
We have covered that boilerplate is generated during a compilation process and presented only in .class files, not .java. It means that while you write code all boilerplate constructors/methods are unavailable for you.
@Data
class Product {
private String name;
private int price;
}
class Main {
public static void main(String[] args) {
Product p = new Product();
p.setName("bread"); // IDE highlihts setName, because it knows nothing about it
}
}
IDE highlights the code above with a message: Cannot resolve method 'setName' in 'Product'. It happens because IDE works with .java code. There is no setName method, it will be generated later on the compilation. The described problem is not a crucial one. You still can compile and run the code using Maven or Gradle directly. However, the problem brings inconvenience and slows down the development. This is a reason why we add a special IDE plugin, which generates Lombok boilerplate for IDE interactively without waiting for a compilation.
We've used 3 Lombok notations already, but let's dig in and explore them explicitly. The @Data annotation packs a pretty big punch because it is a shorthand for five other annotations. They include @ToString, @EqualsAndHashCode, @Getter on all fields, @Setter on all non-final fields, and @RequiredArgsConstructor. Let's briefly explore each one.
The @ToString annotation gives us a toString() method that returns a nicely formatted String with all of the fields contained in it. Say you want to make a Dog, so you created a four-year-old black Border Collie named Ralph, then the toString() method would return a String like this:
Dog { name = 'Ralph', breed = 'Border Collie', age = 4, color = 'black' }
The @EqualsAndHashCode annotation is self-explanatory, provided you understand the hashCode() and equals() methods of the Object class. The short version is that they allow you to compare objects of your class based on the values of their fields, rather than only checking if they have the same object reference. The @Getter annotation provides getters to each field, while the @Setter annotation gives you setters for all of the fields that are not declared final. Remember, if a field is declared final, its value cannot be changed after it is assigned, so it makes sense that Lombok would skip over those.
Constructors: Why so many?
There are three different constructor annotations in Lombok. Why so many, you ask? Well, it turns out having a few different constructors in your tool kit can be pretty helpful depending on your situation. The @RequiredArgsConstructor provides a constructor that only deals with parameters to initialize the fields that are required. A field is considered required if it is declared final but isn't initialized. Or if it is annotated with @NonNull, another Lombok annotation that injects a null check into the constructor. In a nutshell, any field that is allowed to start out as being equal to null is left out of this constructor.
Oftentimes, you'll find yourself in need of a constructor with no parameters; this is especially true if you are working with a database framework like Hibernate, for example. The @NoArgsConstructor will get the job done for you. However, it is worth pointing out that @NoArgsConstructor can potentially cause problems if some fields are required (as defined earlier). You can get around this by using @NoArgsConstructor(force = true). This will automatically initialize final fields with 0, false, or null, depending on their types, and it bypasses the null check in @NonNull.
Finally, we have the @AllArgsConstructor, which, as its name implies, provides a parameter for each field in the class. This is useful if you want to create objects of your class all at once. This results in less code than using a basic constructor and filling in the empty fields using setters.
Conclusion
Hopefully, you can now see why Project Lombok is a big deal for Java developers. Lombok's annotations can reduce the massive amounts of boilerplate code significantly. Often, the single @Data annotation is enough to cut through the clutter and provide everything you need. However, when you need more, annotations such as @AllArgsConstructor, @NoArgsConstructor, @NonNull can give extra functionality without bloating your codebase. If this introduction to Lombok left you wanting more, feel free to dig deeper into Lombok's features to see how else it can help you write more concise Java code.