Boosting productivity in Java: An introduction to Lombok
23 mei 2024 - Jordy Pannemans
As a developer, I’m always excited to find tools or libraries that simplify my work, clean up my code or reduce the amount of code needed. Lombok is such a library which improves the readability of my code by reducing boilerplate code. In this blogpost, we’ll scratch the surface of what Lombok is and how it can be used in Java projects.
What is Lombok?
Lombok is a Java library that eliminates boilerplate code by generating common code constructs during compilation. This is done using annotations to generate getters, setters, constructors, toString methods, and more. Therefore, it’s useful when you want to speed up development and improve readability inside your POJO’s.
Getters and Setters
The two annotations that I use the most are @Getter
and @Setter
. You can
set them at field level to generate getters and setters.
public class Person {
@Getter
@Setter
private String name;
@Getter
@Setter
private int age;
}
Compiled to:
public class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
You can also add these annotations at class level (my preference) to generate getters and setters for all fields of that class. This tightens your code even more. If for example, there are fields that would need no getter or setter method, then you can exclude these fields by specifying the access level.
@Getter
@Setter
public class GetterSetterExample {
private String name;
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private int age;
}
Compiled to:
public class GetterSetterExample {
private String name;
private int age;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
When most of your fields don't need a getter or setter, then it’s preferable to annotate the class fields that do need them. This requires less code and is better for readability.
Constructors
Lombok also provides three annotations to generate constructors to take away the repetitive task of
assigning each argument to its corresponding class attribute. The annotation @NoArgsConstructor
creates a constructor with no parameters. And @AllArgsConstructor
creates one that has
a parameter for each field in your class.
@NoArgsConstructor
@AllArgsConstructor
public class AllArgsNoArgsConstructorsExample {
private String name;
private int age;
}
Compiles to:
public class AllArgsNoArgsConstructorsExample {
private String name;
private int age;
public AllArgsNoArgsConstructorsExample() {
}
public AllArgsNoArgsConstructorsExample(String name, int age) {
this.name = name;
this.age = age;
}
}
Lastly, there’s @RequiredArgsConstructor
which adds a constructor that only takes the
required (final) attributes as parameters.
@RequiredArgsConstructor
public class RequiredArgsConstructorExample {
private final String name;
private int age;
}
Compiles to:
public class RequiredArgsConstructorExample {
private final String name;
private int age;
public RequiredArgsConstructorExample(String name) {
this.name = name;
}
}
toString
Even toString methods can be generated with the @ToString
annotation. This eliminates
the need for manual implementation and allows developers to inspect objects during debugging or
logging.
@ToString
public class ToStringExample {
private String name;
private int age;
}
Compiles to:
public class ToStringExample {
private String name;
private int age;
public String toString() {
return "ToStringExample(name=" + this.name + ", age=" + this.age + ")";
}
}
equals and hashCode
The next annotation we’ll look at is @EqualsAndHashCode
. This annotation generates
equals()
and hashCode()
methods. So, you don’t have to worry about the
complexities of manual implementation of object comparison and hashing.
@EqualsAndHashCode
public class EqualsAndHashCodeExample {
private String name;
private int age;
}
Compiles to:
public class EqualsAndHashCodeExample {
private String name;
private int age;
public boolean equals(final Object o) {
if (o == this) return true;
if (!(o instanceof EqualsAndHashCodeExample)) return false;
EqualsAndHashCodeExample other = (EqualsAndHashCodeExample) o;
if (!other.canEqual((Object) this)) return false;
if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false;
if (this.age != other.age) return false;
return true;
}
protected boolean canEqual(final Object other) {
return other instanceof EqualsAndHashCodeExample;
}
public int hashCode() {
final int PRIME = 59;
int result = 1;
result = result * PRIME + (this.name == null ? 43 : this.name.hashCode());
result = result * PRIME + this.age;
return result;
}
}
Builder
If you decide to use the builder design pattern because of complex objects creation or your class
containing a long list of constructors. Then you could utilize the @Builder
annotation
to make Lombok generate an inner builder class with methods to create objects with a fluent syntax.
@Builder
public class BuilderExample {
private String name;
private int age;
}
Compiles to:
public class BuilderExample {
private String name;
private int age;
BuilderExample(String name, int age) {
this.name = name;
this.age = age;
}
public static BuilderExampleBuilder builder() {
return new BuilderExampleBuilder();
}
public static class BuilderExampleBuilder {
private String name;
private int age;
BuilderExampleBuilder() {
}
public BuilderExampleBuilder name(String name) {
this.name = name;
return this;
}
public BuilderExampleBuilder age(int age) {
this.age = age;
return this;
}
public BuilderExample build() {
return new BuilderExample(this.name, this.age);
}
public String toString() {
return "BuilderExample.BuilderExampleBuilder(name=" + this.name + ", age=" + this.age + ")";
}
}
}
Another use case with @Builder
is to use it on the constructor level. The difference is
that it only uses the parameters of that constructor to generate the inner builder class.
Data
Lombok also provides an annotation that bundles several features with @Data
. It will
generate all the boilerplate code for simple POJOs. This contains @ToString
, @EqualsAndHashCode
,
@RequiredArgsConstructor
, @Getter
for all fields and @Setter
for all non-final fields.
@Data
public class DataExample {
private String name;
private int age;
}
Compiles to:
public class DataExample {
private String name;
private int age;
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public boolean equals(final Object o) {
if (o == this) return true;
if (!(o instanceof DataExample)) return false;
DataExample other = (DataExample) o;
if (!other.canEqual((Object) this)) return false;
if (this.getName() == null ? other.getName() != null : !this.getName().equals(other.getName())) return false;
if (this.getAge() != other.getAge()) return false;
return true;
}
protected boolean canEqual(final Object other) {
return other instanceof DataExample;
}
public int hashCode() {
final int PRIME = 59;
int result = 1;
result = result * PRIME + (this.getName() == null ? 43 : this.getName().hashCode());
result = result * PRIME + this.getAge();
return result;
}
public String toString() {
return "DataExample(name=" + this.getName() + ", age=" + this.getAge() + ")";
}
}
Inheritance
Lombok can be used with inheritance by annotating both super- and subclass with the required annotations. The subclass can call the methods generated in its superclass by Lombok. But the subclass would still need its own annotations for the methods that need to be generated.
The only annotation that needs special care is @Builder
. To use this in the subclass you
must annotate any constructor (that you want a builder for) and use the
builderMethodName
property in the super- or subclass’s annotation. Otherwise, you will
get a compilation error because the subclass then contains two builder()
methods with
different return types.
For example:
@Builder(builderMethodName = "superBuilder")
public class InheritanceSuperExample {
private String name;
private int age;
}
public class InheritanceChildExample extends InheritanceSuperExample {
private String email;
@Builder
public InheritanceChildExample(String name, String email) {
super(name);
this.email = email;
}
}
Which in the subclass compiles to:
public class InheritanceChildExample extends InheritanceSuperExample {
private String email;
public InheritanceChildExample(String name, String email) {
super(name);
this.email = email;
}
public static InheritanceChildExampleBuilder builder() {
return new InheritanceChildExampleBuilder();
}
public static class InheritanceChildExampleBuilder {
private String name;
private String email;
InheritanceChildExampleBuilder() {
}
public InheritanceChildExampleBuilder name(String name) {
this.name = name;
return this;
}
public InheritanceChildExampleBuilder email(String email) {
this.email = email;
return this;
}
public InheritanceChildExample build() {
return new InheritanceChildExample(this.name, this.email);
}
public String toString() {
return "InheritanceChildExample.InheritanceChildExampleBuilder(name=" + this.name + ", email=" + this.email + ")";
}
}
}
Conclusion
Project Lombok accelerates Java development by providing several annotations that reduce boilerplate
code, thus allowing developers to spend more time focusing on implementations that are more complex
and require more attention. By incorporating annotations such as @Getter
,
@Setter
, @ToString
, @EqualsAndHashCode
, @Builder
and the constructor annotations into your projects, you can improve productivity, code readability
and maintainability.