//Introduction to Lombok

Java is often criticised for being unnecessarily verbose compared with other languages. Lombok provides a bunch of annotations that generate boilerplate code in the background, removing it from your classes and therefore helping to keep your code clean. Less boilerplate means more concise code that’s easier to read and maintain. In this post I’ll cover the Lombok features I use more regularly and show you how they can be used to produce cleaner, more concise code.

Local Variable Type Inference – val & var

Lots of languages infer the local variable type by looking at the expression on the righthand side of the equals. Although this is now supported in Java 10+, it wasn’t previously possible without the help of Lombok. The snippet below shows how you have to explicitly specify the local type.

final Map<String, Integer> map = new HashMap<>();
map.put("Joe", 21);

In Lombok we can shorten this by using  val as follows.

val valMap = new HashMap<String, Integer>();
valMap.put("Sam", 30);

Note that under the covers val creates a variable that is final and immutable. If you need a mutable local variable you can use var instead.

@NotNull

Its generally not a bad idea to null check method arguments, especially if the method forms an API being used by other devs. While these checks are straight forward, they can become verbose, especially when you have multiple arguments. As shown below, the added bloat doesn’t help readability and can become a distraction from the main purpose of the method.

public void notNullDemo(Employee employee, Account account){
    
    if(employee == null){
      throw new IllegalArgumentException("Employee is marked @NotNull but is null");
    }
    
    if(account == null){
      throw new IllegalArgumentException("Account is marked @NotNull but is null");
    }
    
    // do stuff
}

Ideally, you want the null check but without the noise. That’s where the @NotNull comes into play. By marking your parameters with  @NotNull Lombok generates a null check for that parameter on your behalf. Your method suddenly becomes much cleaner, but without losing those defensive null checks.

public void notNullDemo(@NotNull Employee employee, @NotNull Account account){
    
      // just do stuff
}

By default Lombok will throw a NullPointerException, but if you want you can configure Lombok to throw an IllegalArgumentException. I personally prefer the IllegalArgumentException as I think its a better fit if you go to the bother of checking the arguments.

Cleaner Data Classes

Data classes is an area where Lombok can really help reduce boilerplate code. Before we look at the options lets consider what kinds of boilerplate we typically have to deal with. A data class typically includes one or all of the following

  • a constructor (without or with arguments)
  • getter methods for private member variables
  • setter methods for private nonfinal member variables
  • toString method to help with logging
  • equals and hashCode (dealing with equality/collections)

The above can be generated by your IDE, so the issue isn’t with the time taken to write them. The problem is that a simple class with a handful of member variables can quickly become very verbose. Let’s see how Lombok can help to reduce clutter by helping with each of the above.

@Getter and @Setter

Consider the Car class below. When we generate getters and setters we end up with nearly 50 lines of code to describe a class with 5 member variables.

public class Car {

  private String make;
  private String model;
  private String bodyType;
  private int yearOfManufacture;
  private int cubicCapacity;

  public String getMake() {
    return make;
  }

  public void setMake(String make) {
    this.make = make;
  }

  public String getModel() {
    return model;
  }

  public void setModel(String model) {
    this.model = model;
  }

  public String getBodyType() {
    return bodyType;
  }

  public void setBodyType(String bodyType) {
    this.bodyType = bodyType;
  }

  public int getYearOfManufacture() {
    return yearOfManufacture;
  }

  public void setYearOfManufacture(int yearOfManufacture) {
    this.yearOfManufacture = yearOfManufacture;
  }

  public int getCubicCapacity() {
    return cubicCapacity;
  }

  public void setCubicCapacity(int cubicCapacity) {
    this.cubicCapacity = cubicCapacity;
  }

}

Lombok can help by generating the getter and setter boilerplate on your behalf. By annotating each member variable with @Getter and @Setter and you end up with an equivalent class that looks like this.

public class Car {

  @Getter @Setter
  private String make;
  @Getter @Setter
  private String model;
  @Getter @Setter
  private String bodyType;
  @Getter @Setter
  private int yearOfManufacture;
  @Getter @Setter
  private int cubicCapacity;
}

Note that you can only use @Setter on non-final member variables. Using it on final member variables will result in a compilation error.

@AllArgsConstructor

Data classes commonly include a constructor that takes a parameter for each member variable. An IDE generated constructor for the Car class is shown below.

public class Car {

  @Getter @Setter
  private String make;
  @Getter @Setter
  private String model;
  @Getter @Setter
  private String bodyType;
  @Getter @Setter
  private int yearOfManufacture;
  @Getter @Setter
  private int cubicCapacity;
  
  public Car(String make, String model, String bodyType, int yearOfManufacture, int cubicCapacity) {
    super();
    this.make = make;
    this.model = model;
    this.bodyType = bodyType;
    this.yearOfManufacture = yearOfManufacture;
    this.cubicCapacity = cubicCapacity;
  }
}

We can achieve the same thing using the @AllArgsConstructor annotation. Like @Getter and @Setter , @AllArgsConstructor reduces boilerplate and keeps the class cleaner and more concise.

@AllArgsConstructor
public class Car {

  @Getter @Setter
  private String make;
  @Getter @Setter
  private String model;
  @Getter @Setter
  private String bodyType;
  @Getter @Setter
  private int yearOfManufacture;
  @Getter @Setter
  private int cubicCapacity;
}

There are other options for generating constructors. @RequiredArgsConstructor will create a constructor with one argument per final member variable and @NoArgsConstructor will create a constructor with no arguments.

@ToString

It’s good practice to override the toString method on your data classes to help with logging. An IDE generated toString method for the  Car class looks like this.

@AllArgsConstructor
public class Car {

  @Getter @Setter
  private String make;
  @Getter @Setter
  private String model;
  @Getter @Setter
  private String bodyType;
  @Getter @Setter
  private int yearOfManufacture;
  @Getter @Setter
  private int cubicCapacity;
  
  @Override
  public String toString() {
    return "Car [make=" + make + ", model=" + model + ", bodyType=" + bodyType + ", yearOfManufacture="
        + yearOfManufacture + ", cubicCapacity=" + cubicCapacity + "]";
  }
}

We can do away with this by using the @ToString annotation as follows.

@ToString
@AllArgsConstructor
public class Car {

  @Getter @Setter
  private String make;
  @Getter @Setter
  private String model;
  @Getter @Setter
  private String bodyType;
  @Getter @Setter
  private int yearOfManufacture;
  @Getter @Setter
  private int cubicCapacity;

By default, Lombok generates a toString method that includes all member variables. This behaviour can be overridden to exclude certain member variables the exclude attribute @ToString(exclude={"someField", "someOtherField"}) .

@EqualsAndHashCode

If you’re doing any kind of object comparison with your data classes you’ll need to override the equals and hashcode methods. Object equality is something you’ll define based on some business rules. For example, in my Car class, I might consider 2 objects equal if they have the same make, model and body type. If I use the IDE to generate an equals method that checks the make, model and body type, it will look something like this.

@Override
public boolean equals(Object obj) {
  if (this == obj)
    return true;
  if (obj == null)
    return false;
  if (getClass() != obj.getClass())
    return false;
  Car other = (Car) obj;
  if (bodyType == null) {
    if (other.bodyType != null)
      return false;
  } else if (!bodyType.equals(other.bodyType))
    return false;
  if (make == null) {
    if (other.make != null)
      return false;
  } else if (!make.equals(other.make))
    return false;
  if (model == null) {
    if (other.model != null)
      return false;
  } else if (!model.equals(other.model))
    return false;
  return true;
}

The equivalent hashCode implementation looks like this.

@Override
public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result + ((bodyType == null) ? 0 : bodyType.hashCode());
  result = prime * result + ((make == null) ? 0 : make.hashCode());
  result = prime * result + ((model == null) ? 0 : model.hashCode());
  return result;
}

Although the IDE takes care of the heavy lifting, we still end up with considerable boilerplate code in the class. Lombok allows us to achieve the same thing using the @EqualsAndHashCode class annotation as shown below.

@ToString
@AllArgsConstructor
@EqualsAndHashCode(exclude = { "yearOfManufacture", "cubicCapacity" })
public class Car {

  @Getter @Setter
  private String make;
  @Getter @Setter
  private String model;
  @Getter @Setter
  private String bodyType;
  @Getter @Setter
  private int yearOfManufacture;
  @Getter @Setter
  private int cubicCapacity;
}

By default @EqualsAndHashCode will create  equals and hashCode methods that include all member variables. The exclude option can be used to tell Lombok to exclude certain member variables. In the code snippet above I’ve excluded yearOfManufacture and  cubicCapacity from the generated equals and hashCode methods.

@Data

If you want to keep your data classes as lean as possible you can make use of the @Data annotation.  @Data is a shortcut for @Getter@Setter, @ToString, @EqualsAndHashCode and @RequiredArgsConstructor

@ToString
@RequiredArgsConstructor
@EqualsAndHashCode(exclude = { "yearOfManufacture", "cubicCapacity" })
public class Car {

  @Getter @Setter
  private String make;
  @Getter @Setter
  private String model;
  @Getter @Setter
  private String bodyType;
  @Getter @Setter
  private int yearOfManufacture;
  @Getter @Setter
  private int cubicCapacity;	
}

By using @Data we can reduce the class above to the following.

@Data
public class Car {

  private String make;
  private String model;
  private String bodyType;
  private int yearOfManufacture;
  private int cubicCapacity;	
}

Object Creation with @Builder

The builder design pattern describes a flexible approach to the creation of objects. Lombok helps you implement this pattern with minimal effort. Let’s look at an example using the simple Car class.  Suppose we want to be able to create a variety of Car objects but we want flexibility in terms of the attributes we set at creation time.

@AllArgsConstructor
public class Car {

  private String make;
  private String model;
  private String bodyType;
  private int yearOfManufacture;
  private int cubicCapacity;	
  private List<LocalDate> serviceDate;
}

Let’s say we want to create a Car but we only want to set the make and model. Using a standard all argument constructor on Car means that we’d supply only make and model and set the other arguments as null.

Car2 car2 = new Car2("Ford", "Mustang", null, null, null, null);

This works but it’s not ideal that we have to pass null for the arguments we’re not interested in. We could get around this by creating a constructor that takes only make and model. This is a reasonable solution but its not very flexible. What if we have lots of different permutations of fields that we might use to create a new Car? We’d end up with a bunch of different constructors representing all the possible ways we could instantiate a Car.

A clean, flexible way to solve this problem is with the builder pattern. Lombok helps you implement the builder pattern via the @Builder annotation. When you annotate the Car class with @Builder , Lombok does the following.

  • Adds a private constructor to Car
  • Creates a static CarBuilder class
  • Creates a setter style method on CarBuilder for each member variable in Car
  • Adds a build method on CarBuilder that creates a new instance of @Car .

Each setter style method on CarBuilder returns an instance of itself (CarBuilder ). This allows you to chain method calls and provides you with a nice fluent API for object creation. Let’s see it in action.

Car muscleCar = Car.builder().make("Ford")
                             .model("mustang")
                             .bodyType("coupe")
                             .build();

Creating a Car with just make and model is now much cleaner than before. We simply call the generated builder method on Car to get an instance of CarBuilder, then call whatever setter style methods we’re interested in. Finally, we call build to create a new instance of Car.

Another handy annotation worth mentioning is @Singular. By default Lombok creates a standard setter style method for collections, that takes a collection argument. In the example below, we create a new Car and set a list of service dates.

Car muscleCar = Car.builder().make("Ford")
                   .model("mustang")
                   .serviceDate(Arrays.asList(LocalDate.of(2016, 5, 4)))
                   .build();

Adding  @Singularto collection member variables gives you an extra method that allows you to add a single item to the collection.

@Builder
public class Car {

  private String make;
  private String model;
  private String bodyType;
  private int yearOfManufacture;
  private int cubicCapacity;	
  @Singular
  private List<LocalDate> serviceDate;
}

We can now add a single service date as follows.

Car muscleCar3 = Car.builder()
                    .make("Ford")
                    .model("mustang")
                    .serviceDate(LocalDate.of(2016, 5, 4))
                    .build();

This is a nice convenience method that helps keep our code clean when dealing with collections during object creation.

Logging

Another great Lombok feature is loggers. WIthout Lombok, to instantiate a standard SLF4J logger you typically have something like this.

public class SomeService {

  private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class);
  
  public void doStuff(){
    
    log.debug("doing stuff....");
  }
}

These loggers are clunky and add unnecessary clutter to every class that requires logging.  Thankfully Lombok provides an annotation that creates the logger for you. All you have to do is add the annotation to the class and you’re good to go.

@Slf4j
public class SomeService {

  public void doStuff(){
    
    log.debug("doing stuff....");
  }
}

I’ve used the @SLF4J annotation here but Lombok will generate loggers for most common java logging frameworks. For more logger options see the documentation.

Lombok Gives You Control

One of the things I really like about Lombok is that it’s unintrusive. If you decide that you want to provide your own method implementation when using the likes of @Getter ,@Setter or @ToString , your method will always take precedence over Lombok. This is nice because it allows you to use Lombok most of the time, but still take control when you need to.

Write Less Do More

I’ve used Lombok on pretty much every project I’ve worked on for the past 4 or 5 years.  I like it because it reduces clutter and you end up with cleaner, more concise code that’s easier to read. It won’t necessarily save you a lot of time, as most of the code it generates can be auto-generated by your IDE. With that said, I think the benefits of cleaner code more than justify adding it to your Java stack.

Further Reading

I covered the Lombok features that I use regularly, but there are a bunch more that I haven’t touched on. If you like what you’ve seen so far and want to find out more, head on over and have a look at the Lombok docs.

By |2019-04-30T08:21:51+00:00April 30th, 2019|Java|0 Comments

Leave A Comment