TL;DR: In this article you will learn how to avoid boilerplate code from your Java projects. You will do so by using features of Project Lombok, a wonderful Java library that was created to help developers speed up. You will learn how to use this library's features by using them on small examples. If you want to see the final code created throughout this article, check the complete
branch of this GitHub repository.
"Learn how to get rid of Java boilerplate code by using Project Lombok."
Tweet This
Prerequisites
To follow this article, you will need to have the following tools installed on your system:
- JDK 8 or any version above.
- Any text editor or IDE of your choice, though I recommend IntelliJ.
- Basic knowledge of the JUnit testing library.
Why Use Project Lombok in your Java project
As you might already know from the opening paragraph, Project Lombok is a Java library. I guess the question you are probably asking yourself is, "Why will I be interested in adding this extra dependency to my classpath?". According to the official Project Lombok site, this library will spice up your Java code by automating the creation of accessor, equals, hashcode, toString methods, and even help you to implement the builder pattern on a class with just one annotation. By automating the creation of these methods on your POJOs (Plain Old Java Objects), Project Lombok will reduce the time spent writing boilerplate code. I will also argue that it will also make your POJOs concise as it will hide the parts that are not that important (from the business point of view) to give more exposure to what makes your POJOs unique.
How Project Lombok's features work
To use any of the Project Lombok's features, you have to add annotations to your classes. The annotations specify which boilerplate code you want Project Lombok to implement for you.
For example, say you have a class and you do not want to implement its toString
method manually. All you have to do is add the @ToString
at the top of your class and, behind the scenes, Project Lombok will use the fields in your class to implement the toString
method. This process is similar for all Project Lombok's features. With that covered, you are all set to start experimenting with Project Lombok.
Playing with Project Lombok
For starters, you will need a Java project to use Lombok. To create one, you could follow several different strategies like using the Spring Initializr website. However, to make things easier, you can simply open a new terminal and clone a GitHub repository that was created for this article:
# clone repo
git clone https://github.com/auth0-blog/lombok.git
# move into it
cd lombok
Then, you will have to open this project in your favorite IDE. After doing so, the first thing you will need to do is to open the build.gradle
file and import the Project Lombok's library into your project:
//.... rest of file
dependencies {
// ... don't remove the other dependencies ...
compileOnly('org.projectlombok:lombok:1.18.4')
annotationProcessor('org.projectlombok:lombok:1.18.4')
}
Note: Your IDE might need a couple of minutes to import the other dependencies and to enable annotation processing if you are planning to run the demos from your IDE.
After adding Lombok into your project, you will need to configure your IDE to generate the boilerplate code for you correctly. The steps to configure this depend on the IDE you are using. Lombok provides some good resources to help you on that matter. On their website, you will find instructions for IntelliJ, Eclipse, Netbeans, and more. Make sure you follow the instructions for your IDE before moving on.
Accessor Annotations
Project Lombok's @Getter
and @Setter
annotations, as their name states, automate the generation of getters and setters for non-static fields of your POJOs. For example, if you have a field called length
, the @Setter
annotation will create a method called setLength
to allow developers to change this value. The @Getter
annotation, on the other hand, will create a getLength
method which will return the value for whoever is asking.
Note: As Lombok follows conventions, if
length
is a boolean, Lombok will call the getter accessor asisLength
instead of calling itgetLength
.
There are other interesting Lombok's behaviors that you should know about:
- If you place these annotations on a class field, Project Lombok will automatically generate the accessor methods for you.
- If these annotations are placed on the class, Project Lombok will create accessor methods for all non-static fields of the class. In this case, you can also control the access level of the methods generated by passing
AccessLevel.PROTECTED
,AccessLevel.PUBLIC
,AccessLevel.PRIVATE
,AccessLevel.PACKAGE
as an argument to the accessor annotations.
With the theory covered, now is the time to put all these to practice. Imagine you have an app and you want to create a POJO for your app's users. To do that, you would create a class called User
. Then, you would add code similar to this on it:
public class User {
private String username;
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
This implementation would work just fine but imagine you would add a dozen more attributes to your class. How would that look? Quite big, wouldn't it? To avoid that, you can use the Project Lombok's annotations you just learned about to automatically generate the getters and setters above.
To see this in action, create the User
class inside the com.auth0.lomboktest
package and add the following code to it:
package com.auth0.lomboktest;
import lombok.Getter;
import lombok.Setter;
public class User {
@Getter @Setter
private String username;
@Getter @Setter
private String password;
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
Do you see how, by using these Lombok's annotations, you have dramatically reduced the amount of code in this class? Both versions presented of the User
class are equivalent. The difference is that Project Lombok will generate the getters and setters for you because you used Lombok's annotations on the username
and password
fields.
To confirm that this is really working, open the LomboktestApplicationTests
class (you will find it inside the ./src/test/java/com/auth0/lomboktest/
directory) and update it as follows:
package com.auth0.lomboktest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class LomboktestApplicationTests {
@Test
void testAccessor() {
String username = "some-user";
String password = "some-password";
User user = new User(username, password);
assertEquals(username, user.getUsername());
assertEquals(password, user.getPassword());
}
}
Then, back on the terminal, issue the following command:
./gradlew test
If everything is in place, you should see, among other things, the following output:
> Task :test
com.auth0.lomboktest.LomboktestApplicationTests
Test testAccessor() PASSED
SUCCESS: Executed 1 tests in 730ms
Builder Annotation
Apart from helping you generate accessor methods, as mentioned, Project Lombok can also help you implement the builder design pattern. As an example, imagine you would create a new class called Course
that implements the builder pattern. This is what this class would look like:
public class Course {
@Getter @Setter
private String courseName;
@Getter @Setter
private String courseCode;
public Course(String courseName, String courseCode){
this.courseCode = courseCode;
this.courseName = courseName;
}
public static CourseBuilder builder() {
return new CourseBuilder();
}
public static class CourseBuilder {
private String courseName;
private String courseCode;
public CourseBuilder courseName(String courseName){
this.courseName = courseName;
return this;
}
public CourseBuilder courseCode(String courseCode){
this.courseCode = courseCode;
return this;
}
public Course build(){
return new Course(courseName, courseCode);
}
}
}
As you can see, this class uses the inner CourseBuilder
class to implement the builder pattern.
Now, to learn about how to replace all this boilerplate code with the help of Lombok, create the Course
class in the com.auth0.lomboktest
package and add the following code to it:
package com.auth0.lomboktest;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Builder
public class Course {
@Getter @Setter
private String courseName;
@Getter @Setter
private String courseCode;
}
With just the @Builder annotation, Lombok will help you achieve the same result you got on the previous version of the Course
class. That is, you replaced 20 lines (or so) of boilerplate code by a single annotation. Not to mention that you got this difference in a very simple class with just two fields (courseName
and courseCode
). Imagine the difference in a class with a dozen fields... Awesome, isn't it?
Now, to test your builder, update the LomboktestApplicationTests
class as follows:
package com.auth0.lomboktest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
class LomboktestApplicationTests {
// ... leave testAccessor untouched ...
@Test
void testBuilder() {
String courseName = "Intro to CS";
String courseCode = "cs50";
Course course = Course.builder().courseName(courseName).courseCode(courseCode).build();
assertEquals(courseName, course.getCourseName());
assertEquals(courseCode, course.getCourseCode());
}
}
Then, if you run ./gradlew test
in your terminal, you should see that testBuilder()
passed successfully.
The Cleanup Annotation
Prepending any resource variable (e.g., FileInputStream
) with @Cleanup
will tell Project Lombok to automatically close this resource once it is no longer in use in the current scope.
Note: As of Java 7.0, there is a special
try
statement that automatically does resource management for you.
To demonstrate how to use this annotation, imagine a class where you read from an input file and write to an output file. This class would go more or less like this:
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class VanillaIO {
public VanillaIO() throws IOException {
InputStream in = null;
try {
in = new FileInputStream("input.txt");
OutputStream out = null;
try {
out = new FileOutputStream("output.txt");
byte[] b = new byte[10000];
while (true) {
int r = in.read(b);
if (r == -1) break;
out.write(b, 0, r);
}
} finally {
out.close();
}
} finally {
in.close();
}
}
}
With Lombok, you can avoid a lot of the boilerplate code above just by using the @Cleanup
annotation, as you can see in the following code:
import lombok.Cleanup;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class LombokIO {
public LombokIO() throws IOException {
@Cleanup InputStream in = new FileInputStream("input.txt");
@Cleanup OutputStream out = new FileOutputStream("output.txt");
byte[] b = new byte[10000];
while (true) {
int r = in.read(b);
if (r == -1) break;
out.write(b, 0, r);
}
}
}
Notice how this annotation reduces the code to a few lines of code and makes it way cleaner. Cool, isn't it?
ToString Annotation
Another cool annotation to learn about is the @ToString
one. Imagine you have a class called Person
and that you would like to override its toString
method to something more meaningful. On a normal day, you would have code similar to this:
public class Person {
private String firstName;
private String lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return "Person(firstName=" + firstName + ", lastName=" + lastName + ")";
}
}
With Lombok, you can simply use the @ToString
annotation, and that's it:
import lombok.ToString;
@ToString
public class Person {
private String firstName;
private String lastName;
public Person(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
For two field, this might not look like a great deal. However, when you reach a dozen fields, you will see the difference.
To see this in action, create the class above inside the com.auth0.lomboktest
package, then update the LomboktestApplicationTests
test class as follows:
class LomboktestApplicationTests {
// ... testAccessor and testBuilder ...
@Test
void testToString() {
String firstName = "John";
String lastName = "Doe";
String expectedResult = "Person(firstName=" + firstName + ", lastName=" + lastName + ")";
Person person = new Person(firstName, lastName);
assertEquals(expectedResult, person.toString());
}
}
Now, if you run ./gradlew test
, you will see that Gradle executes three tests successfully.
The EqualsAndHashCode Annotation
This is, perhaps, one of the coolest features that Lombok introduces. By using the @EqualsAndHashCode
annotation in your classes, you make Lombok generate the equals
and hashcode
methods for you.
Imagine that you have a class called Lecture
and that you have to implement the equals
and hashcode
to define a custom equality-checking mechanism. Without Lombok, you would probably do something like:
import java.util.Objects;
public class Lecture {
private String lectureName;
private String lectureCode;
public Lecture(String lectureName, String lectureCode) {
this.lectureName = lectureName;
this.lectureCode = lectureCode;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Lecture that = (Lecture) o;
return Objects.equals(lectureName, that.lectureName) &&
Objects.equals(lectureCode, that.lectureCode);
}
@Override
public int hashCode() {
return Objects.hash(lectureName, lectureCode);
}
}
Not the most beautiful, or readable, code, right? Compare this with the version that Lombok allows:
package com.auth0.lomboktest;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode
public class Lecture {
private String lectureName;
private String lectureCode;
public Lecture(String lectureName, String lectureCode) {
this.lectureName = lectureName;
this.lectureCode = lectureCode;
}
}
Yup, that's it! The equals
and hashcode
methods disappeared from your code and are now created by Lombok.
To see it in action, update the LomboktestApplicationTests
class as follows:
class LomboktestApplicationTests {
// ... other test methods ...
@Test
void testEqualsAndHashCode() {
String lectureName = "Intro to Computer Science";
String lectureCode = "cs50";
Lecture l1 = new Lecture(lectureName, lectureCode);
Lecture l2 = new Lecture(lectureName, lectureCode);
assertEquals(l1, l2);
}
}
Then, run ./gradlew test
. This should print that four tests were successfully executed.
The NonNull Annotation
When the @NonNull
annotation is placed before a method parameter, it means that this method will not accept null
as an argument. If you pass to it a null object, the method will automatically raise a NullPointerException
(NPE).
By using this annotation, you avoid having to manually check and to manually raise NPEs. For example, if you had a Customer
class that didn't accept null for the name
field, instead of doing this:
public class Customer {
private String name;
private String phone;
public Customer(String name, String phone) {
if (name == null) {
throw new NullPointerException("Customer name required.");
}
this.name = name;
this.phone = phone;
}
}
You would do this:
import lombok.NonNull;
public class Customer {
private String name;
private String phone;
public Customer(@NonNull String name, String phone) {
this.name = name;
this.phone = phone;
}
}
Want to see this in action? Just create the above class inside the lomboktest
package then update the LomboktestApplicationTests
method as follows:
// ... package name and other import statements ...
import static org.junit.jupiter.api.Assertions.assertThrows;
class LomboktestApplicationTests {
// ... other test methods ...
@Test
void testNullPointerExceptions() {
Customer okCustomer = new Customer("Someone", null);
assertThrows(NullPointerException.class, ()->{new Customer(null, null);});
}
}
Then, after you run ./gradlew test
, you will see that five tests were executed successfully.
The Constructor Annotations
In this section, you will learn about two annotations: @NoArgsConstructor
and @AllArgsConstructor
. As their name states, the first annotation creates an empty constructor (no arguments on it) and the second one creates a constructor that contains all the fields on the class.
You probably already know the drill, right? So, instead of seeing how you would do on a vanilla-Java class, you can skip and create a class called Product
inside the com.auth0.lomboktest
package. Then, add the following code to this class:
package com.auth0.lomboktest;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Product {
private String title;
private String description;
private boolean available;
private BigDecimal price;
private BigDecimal weight;
}
In this case, just by adding @NoArgsConstructor
and @AllArgsConstructor
, you make Lombok create an empty constructor and also a constructor that get values for all the five fields defined (title
, description
, available
, price
, and weight
).
To test these annotations, update the LomboktestApplicationTests
class as follows:
// ... package name and other import statements ...
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.math.BigDecimal;
class LomboktestApplicationTests {
// ... other test methods ...
@Test
void testConstructors() {
Product unknown = new Product();
assertNull(unknown.getTitle());
assertNull(unknown.getDescription());
assertFalse(unknown.isAvailable());
assertNull(unknown.getPrice());
assertNull(unknown.getWeight());
String title = "Pizza";
String description = "Delicious";
boolean available = true;
BigDecimal price = BigDecimal.valueOf(15);
BigDecimal weight = BigDecimal.valueOf(0.9);
Product pizza = new Product(title, description, available, price, weight);
assertEquals(title, pizza.getTitle());
assertEquals(description, pizza.getDescription());
assertTrue(pizza.isAvailable());
assertEquals(price, pizza.getPrice());
assertEquals(weight, pizza.getWeight());
}
}
Then, run ./gradlew test
. If everything is in place, you will see that six tests were executed successfully.
Lombok also provides another annotation called @RequiredArgsConstructor
. This annotation generates a constructor with one parameter for each field that requires special handling. All non-initialized final fields get a parameter, as well as any fields that are marked as @NonNull that aren't initialized where they are declared. Take a look here to learn more about it.
The Data Annotation
The last annotation that you will learn about in this article is the @Data
one. This annotation is a handy combination of the @ToString
, @EqualsAndHashCode
, @Getter
, @Setter
and @RequiredArgsConstructor
annotations. That is, by using this annotation, you make Lombok:
- override the
toString
method; - define the
equals
andhashcode
methods; - create getters and setters for your class' fields;
- define a constructor for all non-initialized and non-nullable fields;
To see this annotation in action, create a class called Magic
inside the com.auth0.lomboktest
package with the following code:
package com.auth0.lomboktest;
import lombok.Data;
import lombok.NonNull;
@Data
public class Magic {
private @NonNull String title;
private @NonNull Integer grade;
private boolean works = true;
private String description;
}
Then, update the LomboktestApplicationTests
class as follows:
class LomboktestApplicationTests {
// ... other test methods ...
@Test
void testData() {
String title = "Lombok";
Integer grade = 10;
Magic m1 = new Magic(title, grade);
Magic m2 = new Magic(title, grade);
assertEquals(m1, m2);
assertEquals(title, m1.getTitle());
assertEquals(grade, m2.getGrade());
assertTrue(m1.isWorks());
}
}
Running ./gradlew test
now should output that seven tests were executed successfully. This proves that @Data
is capable of creating comparable objects (@EqualsAndHashCode
), classes that contain constructors with the required fields (@RequiredArgsConstructor
), and that it also generates the getters and setters for you. Cool, huh?
"Lombok helps us to replace a looot of Java boilerplate code by just a few annotations."
Tweet This
Conclusion
In this article, you learned how to use Project Lombok's annotations to remove boilerplate code from your project. In the sections above you learned about cool features provided by Lombok like @Data
, @EqualsAndHashCode
, constructor annotations, @Builder
, and a few others.
On their own, are these features useful enough to make you use Lombok on your next project? Even if they are, be aware, Lombok includes a few other features that might be handy. Check this resource for a complete list of Lombok's features.