Introduction to Lombok
Check out the major benefits and features of Lombok.
Join the DZone community and get the full member experience.
Join For FreeJava is often criticized for being unnecessarily verbose when 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 and var
Lots of languages infer the local variable type by looking at the expression on the right-hand 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.
@NonNull
It's 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 straightforward, 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 nonNullDemo(Employee employee, Account account){
if(employee == null){
throw new IllegalArgumentException("Employee is marked @NonNull but is null");
}
if(account == null){
throw new IllegalArgumentException("Account is marked @NonNull but is null");
}
// do stuff
}
Ideally, you want the null check — without all the noise. That’s where the @NonNull
comes into play. By marking your parameters with @NonNull
, 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 nonNullDemo(@NonNull Employee employee, @NonNull 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 are an area where Lombok can really help reduce boilerplate code. Before we look at the options, let's 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 loggingequals
andhashCode
(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
, 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.
If you need a getter and setter for each member variable, you can also use @Getter
and @Setter
at the class level as follows.
@Getter
@Setter
public class Car {
private String make;
private String model;
private String bodyType;
private int yearOfManufacture;
private int cubicCapacity;
}
@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 behavior 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 two 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 that 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 inCar
- 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 a 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 @Singular
to collection member variables give 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've 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.
Published at DZone with permission of Brian Hannaway, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments