close icon
Java

Avoiding Java Boilerplate Code with Project Lombok

This article demonstrates how you can use Project Lombok library to avoid boilerplate code from your Java projects.

January 30, 2019

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

Tweet This


Prerequisites

To follow this article, you will need to have the following tools installed on your system:

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 as isLength instead of calling it getLength.

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 and hashcode 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

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.

  • Twitter icon
  • LinkedIn icon
  • Faceboook icon