Reducing Boilerplate Code With Annotations
Learn more about how Lombok annotations can reduce boilerplate code in your applications.
Join the DZone community and get the full member experience.
Join For FreeOne of the most common criticisms of Java is its verbosity — and rightfully so. Not only can it be tedious to write repetitive methods, such as getters and setters, but it can be difficult to write equals
and hashCode
methods correctly. Each of these methods has the same general structure, but it depends on the particular fields of a class. For example, writing getters for a class requires that the following structure be repeated for each field:
private T field;
public T getField() {
return field;
}
While most Integrated Development Environments (IDEs) include tools for automatically generating these methods, this still does not solve some of the more egregious problems. For example, when adding a new field to a class, the author of that class must remember to generate a new getter for the field (if all fields are expected to have a getter). This problem is exacerbated when implementing hashCode
methods, which depend on the order of the fields in a class.
To resolve these issues of boilerplate code, the Lombok Project has devised a set of annotations that can be used to automatically generate many of the standard methods associated with Java classes. This technique differs significantly from the IDE approach: Lombok generates bytecode at compile-time, removing the need for a developer to manually generate source code to be compiled at a later time. In this article, we will cover some of the basic features of Lombok, including generating getters, setters, equals
, and hashCode
methods. We will also cover some of the under-the-hood details of how Lombok accomplishes this through simple annotation processing. While there are countless other features supported by Lombok, comprehensively covering each would be prohibitive, and instead, resources are provided at the end of this article for the interested reader to learn more about the various annotations supported by Lombok.
Replacing Boilerplate Code
The purpose of Lombok is to replace the annoyance and fragility of manually defining monotonous methods and instead generate these methods during compilation. Beyond this reduction in manual programming, Lombok allows developers to remove much of the clutter associated with common class methods. For example, it is easy to lose sight of the purpose and responsibility of classes that resemble the following:
@FunctionalInterface
public interface Database<T> {
public T getById(long id);
}
public class Service<T> {
private final long id;
private final String name;
private final Database<T> database;
public Service(long id, String name, Database<T> database) {
this.id = id;
this.name = name;
this.database = database;
}
public T getById(long id) {
return database.getById(id);
}
public long getId() {
return id;
}
public String getName() {
return name;
}
public Database<T> getDatabase() {
return database;
}
@Override
public int hashCode() {
return Objects.hash(id, name, database);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Service)) {
return false;
}
else {
Service other = (Service) obj;
return Objects.equals(database, other.database)
&& id == other.id
&& name.equals(name);
}
}
@Override
public String toString() {
return "Service(id=" + id + ", name=" + name + ", database=" + database + ")";
}
}
The purpose of the Service
class is to act as a proxy for the Database
class associated with some storable type, T
, but due to the overwhelming number of boilerplate lines, its true purpose is easily obscured. Using Lombok, we can reduce this same class to the following:
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {
private final long id;
private final String name;
private final Database<T> database;
public T getById(long id) {
return database.getById(id);
}
}
We can then test this Service
class using the following snippet:
Service<Object> service = new Service<Object>(1, "SomeService", id -> { return null; });
System.out.println("Name of service: " + service.getName());
While this may look like a sleight of hand — since the Service
constructor and getName
method does not exist in our code — the expected methods are generated during compilation, so long as the Lombok JAR is present on the classpath. For example, if the Service
class was present in a Maven application, we would need to add the following dependency to the pom.xml
file (using 1.18.8, the latest version at the time of writing; check MVN Repository for latest version):
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
<scope>provided</scope>
</dependency>
We can then build the JAR, containing our Database
, Service
, and test snippet, using the following command:
mvn clean package
If we execute our generated JAR, we see the following output:
Name of service: SomeService
As we will see shortly, the Lombok library processed our annotations and generated corresponding bytecode, allowing us to call methods that did not otherwise exist (namely, the Service
constructor and getName
). To verify that these methods were properly generated, we can inspect the compiled Service.class
file within our generated JAR using the javap -c Service.class
command:
public class com.dzone.albanoj2.lombok.Service<T> {
public T getById(long);
Code:
...
public long getId();
Code:
...
public java.lang.String getName();
Code:
...
public com.dzone.albanoj2.lombok.Database<T> getDatabase();
Code:
...
public Service(long, java.lang.String, com.dzone.albanoj2.lombok.Database<T>);
Code:
...
public boolean equals(java.lang.Object);
Code:
...
protected boolean canEqual(java.lang.Object);
Code:
...
public int hashCode();
Code:
...
public java.lang.String toString();
Code:
...
}
As we can see in the bytecode above, our Service
class has a getter for each of its fields, a constructor that includes parameters for each of its required fields, an equals method, a canEqual
method, a hashCode
method, and a toString
method. Each of these constructors and method has a particular meaning, mirroring their manually programmed analogs.
@Getter
This annotation generates a getter method for each of the fields in the annotated class. For example, applying this annotation to the Service
class generates bytecode that is effectively equal to the following source code:
public long getId() {
return id;
}
public String getName() {
return name;
}
public Database<T> getDatabase() {
return database;
}
This can be verified by inspecting the bytecode for each of the getters, which performs a simple getfield
for each of the fields in the Service
class:
public long getId();
Code:
0: aload_0
1: getfield #13 // Field id:J
4: lreturn
public java.lang.String getName();
Code:
0: aload_0
1: getfield #14 // Field name:Ljava/lang/String;
4: areturn
public Database<T> getDatabase();
Code:
0: aload_0
1: getfield #1 // Field database:Lcom/dzone/albanoj2/lombok/Database;
4: areturn
Additionally, if only certain fields should have getters, the @Getter
annotation can be removed from the class level and applied to individual fields. For example, if only the name
field of the Service
class should have a getter, the Service
class could be rewritten as:
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {
private final long id;
@Getter private final String name;
private final Database<T> database;
public T getById(long id) {
return database.getById(id);
}
}
An explicit getter can also be added to the class, which keeps Lombok from generating a getter (it refers to the explicit getter). For example, we can create the following class:
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {
private final long id;
private final String name;
private final Database<T> database;
public T getById(long id) {
return database.getById(id);
}
public String getName() {
return name + "extra";
}
}
Looking at the bytecode generated for the Service
class, we can see that the getter we explicitly created for name
is maintained, while the default getter is generated for id
and database
:
public java.lang.String getName();
Code:
0: new #3 // class java/lang/StringBuilder
3: dup
4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
7: aload_0
8: getfield #5 // Field name:Ljava/lang/String;
11: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #7 // String extra
16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
22: areturn
public long getId();
Code:
0: aload_0
1: getfield #9 // Field id:J
4: lreturn
public com.dzone.albanoj2.lombok.Database<T> getDatabase();
Code:
0: aload_0
1: getfield #1 // Field database:Lcom/dzone/albanoj2/lombok/Database;
4: areturn
If needed, we can also change the access level of the getters by supplying a value of the AccessLevel
enumeration to the @Getter
annotation (by default, the access level is set to public
). For example, we could specify that the getters for the Service
class should be protected
, rather than public
:
@Getter(AccessLevel.PROTECTED)
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {
private long id;
private final String name;
private final Database<T> database;
public T getById(long id) {
return database.getById(id);
}
}
Looking at the bytecode for the Service
class, we see that this access level is reflected in the getters generated for the Service
class:
protected long getId();
Code:
0: aload_0
1: getfield #3 // Field id:J
4: lreturn
protected java.lang.String getName();
Code:
0: aload_0
1: getfield #4 // Field name:Ljava/lang/String;
4: areturn
protected com.dzone.albanoj2.lombok.Database<T> getDatabase();
Code:
0: aload_0
1: getfield #1 // Field database:Lcom/dzone/albanoj2/lombok/Database;
4: areturn
@Setter
Similar to generating getters, Lombok can also generate setters. In the case of our Service
class, no setters that can be generated, since all fields are final
. For demonstration, we can remove the final
modifier from the id
field, which will allow us to create a setter for it. Once this modifier has been removed, we can apply the @Setter
annotation to the Service
class:
@Getter
@Setter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {
private long id;
private final String name;
private final Database<T> database;
public T getById(long id) {
return database.getById(id);
}
}
Lombok will now generate a setter for the id
field only. If we look at the bytecode for the Service
class, we see that this setter does what we expect from a setter: Assign the parameter value to the field — in this case, id
— associated with the setter:
public void setId(long);
Code:
0: aload_0
1: lload_1
2: putfield #3 // Field id:J
5: return
As with the @Getter
annotation, we can also specify the access level of the generated setter methods by supplying a value of the AccessLevel
enumeration to the @Setter
annotation. We can also apply the @Setter
annotation to individual fields, rather than the entire class, if only certain fields should have a setter method.
@RequiredArgsConstructor
Since we have marked each of the fields in the Service
class final
, they must be set through the Service
constructor (in actuality, any constructor declared for Service
). By using the @RequiredArgsConstructor
, Lombok generates a constructor that has parameters for each of the required (final
) fields and sets each of the fields to its corresponding parameter. This effectively generates the following for our Service
class:
public Service(long id, String name, Database<T> database) {
this.id = id;
this.name = name;
this.database = database;
}
We can verify this by inspecting the bytecode generated for the required argument constructor, which calls the Object
default constructor (java/lang/Object." ":()V
) — as required by all classes, since all classes without explicit superclasses are subclasses of Object
— and then sets each of the fields by performing a simple putfield
:
public com.dzone.albanoj2.lombok.Service(long, java.lang.String, com.dzone.albanoj2.lombok.Database<T>);
Code:
0: aload_0
1: invokespecial #5 // Method java/lang/Object."<init>":()V
4: aload_0
5: lload_1
6: putfield #3 // Field id:J
9: aload_0
10: aload_3
11: putfield #4 // Field name:Ljava/lang/String;
14: aload_0
15: aload 4
17: putfield #1 // Field database:Lcom/dzone/albanoj2/lombok/Database;
20: return
@AllArgsConstructor
Similar to the @RequiredArgsConstructor
annotation, the @AllArgsConstructor
annotation generates a constructor that includes parameters for all fields, not just those with the final
modifier. In the case of our Service
class, the @RequiredArgsConstructor
and @AllArgsConstructor
will produce the same constructor, since all fields are final
, but change the constructor generated through the @RequiredArgsConstructor
annotation by making the id
field mutable:
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {
private long id;
private final String name;
private final Database<T> database;
public T getById(long id) {
return database.getById(id);
}
}
Using this updated Service
implementation, the @RequiredArgsConstructor
will generate a constructor of the following form:
public Service(String name, Database<T> database) {
this.name = name;
this.database = database;
}
On the other hand, the @AllArgsConstructor
will generate a constructor of the following form:
public Service(long id, String name, Database<T> database) {
this.id = id;
this.name = name;
this.database = database;
}
Inspecting the bytecode generated for the @AllArgsConstructor
annotation, we see that it is identical to that of the constructor generated for @RequiredArgsConstructor
when all fields were final
:
public com.dzone.albanoj2.lombok.Service(long, java.lang.String, com.dzone.albanoj2.lombok.Database<T>);
Code:
0: aload_0
1: invokespecial #5 // Method java/lang/Object."<init>":()V
4: aload_0
5: lload_1
6: putfield #3 // Field id:J
9: aload_0
10: aload_3
11: putfield #4 // Field name:Ljava/lang/String;
14: aload_0
15: aload 4
17: putfield #1 // Field database:Lcom/dzone/albanoj2/lombok/Database;
20: return
@NoArgsConstructor
The last of the constructor options we can generate is the default — or no-arg — constructor. Since we are generating constructors for our Service
class, a default constructor will not be created by the compiler. Instead, we have to generate one ourselves. In the case of our Service
class, we cannot apply the @NoArgsConstructor
annotation, since we have final
fields, or the following compilation error will occur when we attempt to build our application:
variable id might not have been initialized
This can be remedied by removing the final
modifier from all fields and applying the @NoArgsConstructor
annotation to the class:
@Getter
@NoArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {
private long id;
private String name;
private Database<T> database;
public T getById(long id) {
return database.getById(id);
}
}
Lombok will then generate a constructor of the following form:
public Service() {}
We can confirm this by inspecting the bytecode generated for our Service
class, where the generated no-args constructor simply calls the Object
constructor:
public com.dzone.albanoj2.lombok.Service();
Code:
0: aload_0
1: invokespecial #42 // Method java/lang/Object."<init>":()V
4: return
@EqualsAndHashCode
Equality is an essential aspect of a class definition — whether creating a value object or leveraging the equality semantics built into Java, such as adding an element to a Set
— but it can be tricky to implement correctly. Additionally, Java expects that the equals
and hashCode
methods follow a standard set of guidelines when defining the equality for a class, such as equal objects always producing equal hashCode
values (see Items 10 and 11 in Effective Java, 3rd Edition). For example, according to the Object
class documentation, the equals method must have the following characteristics:
- It is reflexive: for any non-null reference value
x
,x.equals(x)
should returntrue
. - It is symmetric: for any non-null reference values
x
andy
,x.equals(y)
should returntrue
if and only ify.equals(x)
returnstrue
. - It is transitive: for any non-null reference values
x
,y
, andz
, ifx.equals(y)
returnstrue
andy.equals(z)
returnstrue
, thenx.equals(z)
should returntrue
. - It is consistent: for any non-null reference values
x
andy
, multiple invocations ofx.equals(y)
consistently returntrue
or consistently returnfalse
, provided no information used inequals
comparisons on the objects is modified. - For any non-null reference value
x
, x.equals(null) should returnfalse
.
In most cases, our IDE can be used to generate these methods, but this can be troublesome, as adding new fields requires that the existing equals
and hashCode
methods are regenerated, and apart of futureproofing, the code generated by IDEs can be challenging to understand. For example, given our definition of the Service
class, the following are the equals
and hashCode
methods generated by Eclipse:
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((database == null) ? 0 : database.hashCode());
result = prime * result + (int) (id ^ (id >>> 32));
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Service other = (Service) obj;
if (database == null) {
if (other.database != null)
return false;
} else if (!database.equals(other.database))
return false;
if (id != other.id)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
return true;
}
The use of primes in the hashCode
method is correct, as it imposes order when comparing the fields — such that if an object has two fields, a and b, of the same type, a of the first object equaling b of the second object and b of the first object equaling a of the second object, do not result in the same hashCode
value. Likewise, in the equals
method, there are various checks for the type of the object being compared to and numerous checks that the fields of both objects are not null
.
This code is required to create a correct definition of equality, but it clutters our code with intricate implementation details. Instead, it is preferable to denote that our class should have equals
and hashCode
methods generated and specify only which fields should be included. We can do this using the @EqualsAndHashCode
annotation in Lombok:
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {
private final long id;
private final String name;
private final Database<T> database;
public T getById(long id) {
return database.getById(id);
}
}
Apart from generating equals
and hashCode
methods, Lombok also generates a canEquals
method that is used for proper equality comparisons with classes that are subclasses of classes other than Object (for more information on canEquals
, see How to Write an Equality Method in Java). Note that if a getter is available for a field, the value returned from the getter will be used in the equality methods rather than the field value directly.
Additionally, fields can be excluded from the equality comparison — in both the equals
and hashCode
methods — by annotating a field with @EqualsAndHashCode.Exclude
. For example, if we did not want the database
field to be included in the equality comparison of our Service
class, we could do the following:
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {
private final long id;
private final String name;
@EqualsAndHashCode.Exclude private final Database<T> database;
public T getById(long id) {
return database.getById(id);
}
}
Note that equality can become tricky when dealing with superclasses. Being that the semantics of the equality comparison is abstracted behind Lombok, the @EqualsAndHashCode
annotation includes various flags to allow developers to dictate the behavior of the generated equals and hashCode
methods in a non-trivial type hierarchy. For more information, see the official @EqualsAndHashCode documentation.
@ToString
Overriding the toString
method for a class is an important task — to ensure that objects are printed in a human-readable manner — but doing so can be tedious. In general, toString
methods follow the same pattern: The name of the class (without its package) followed by the current value of the fields of the object, usually prepended by the name of the field. Since this pattern is repeated, IDEs can generate a boilerplate implementation of toString
; while IDEs do have shortcuts for generating implementations of the toString
method, the generated method must be updated each time the fields of the class change. Additionally, the generated toString
method also introduces clutter in the class definition, which reduces the readability of the class as a whole.
To reduce the clutter, Lombok includes the @ToString
annotation, which instructs the Lombok framework to generate a toString
implementation on our behalf. By default, the implementation includes the name of the class and the current value of each field (with the name of the field). For example, we can create the following class:
@RequiredArgsConstructor
@ToString
public class Service<T> {
private final long id;
private final String name;
private final Database<T> database;
// ... constructors and methods ...
}
public class Database<T> {
// ... implementation ...
}
public class Foo {}
Instantiating an object using new Service (1L, "blah", id -> null)
and calling toString
on that object results in the following output (the package and ID of the Database<T>
lambda expression will vary depending on the project structure and execution):
Service(id=1, name=blah, database=com.dzone.albanoj2.lombok.Main$$Lambda$1/0x0000000800060c40@12f40c25)
It is important to note that if a getter is provided for a field, the getter will be used to print the field rather the value of the field. This use of getters means we manipulate the value of a field before it is printed to using the toString
method generated by Lombok. For example, we can create the following class:
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {
private final long id;
private final String name;
private final Database<T> database;
public T getById(long id) {
return database.getById(id);
}
public String getName() {
return name + " plus extra";
}
}
Instantiating and calling toString
on the object again results in the following output:
Service(id=1, name=blah plus extra, database=com.dzone.albanoj2.lombok.Main$$Lambda$1/0x0000000800060c40@12f40c25)
To exclude a field from the toString
implementation, the @ToString.Exclude
annotation can be applied to the desired field. For example, we could create the following class:
@Getter
@RequiredArgsConstructor
@EqualsAndHashCode
@ToString
public class Service<T> {
private final long id;
private final String name;
@ToString.Exclude
private final Database<T> database;
public T getById(long id) {
return database.getById(id);
}
}
Instantiating an object and then calling the toString
on that object results in the following output:
Service(id=1, name=blah)
Apart from excluding fields, the @ToString
annotation includes other features, such as:
- Applying the
onlyExplicitlyIncluded
flag (i.e.@ToString(onlyExplicitlyIncluded = true)
) instructs Lombok to include only those fields that are explicitly annotated with@ToString.Include
- Applying the
callSuper
flag (i.e.,@ToString(callSuper = true)
) instructs Lombok to include thetoString
output from the superclass as the first field, with the field namesuper
(if configured to include the field name—see below) - Applying the name field to the
@ToString.Include
annotation (i.e.,@ToString.Include(name = "something")
) instructs Lombok to use the explicitly provided name as the field name; for example, if@ToString.Include(name = "something")
is applied to thename
field above in the lastService
implementation, the output would beService(id=1, something=blah)
- Disabling the
includeFieldNames
flag (i.e.@ToString(includeFieldNames = false)
) instructs Lombok to exclude the field names from thetoString
output; for example, if theincludeFieldNames
flag were set to false in the lastService
implementation, the output would beService(1, blah)
For more information, see the official Lombok @ToString
documentation and @ToString
JavaDocs.
How it Works
As discussed in Creating Annotations in Java, annotations do not provide any logic in-and-of themselves. Instead, additional code is needed to process annotations and perform some actions based on the annotations. To aid in de-coupling the processing logic from the annotations themselves, Java introduced the Pluggable Annotation Processing API in JSR 269, which allows JARs to register annotation processors that can consume annotations and perform actions based on the annotations encountered.
In particular, Lombok includes the following contents in its META-INF/services/javax.annotation.processing.Processor
file:
lombok.launch.AnnotationProcessorHider$AnnotationProcessor
lombok.launch.AnnotationProcessorHider$ClaimingProcessor
This file allows the lombok.launch.nnotationProcessor
class to be registered as an annotation processor, which eventually calls the lombok.core.AnnotationProcessor
class, which in turn uses the lombok.javac.api.LombokProcessor
class (APT stands for Annotation Processing Tool). These processors work in tandem to inspect the annotations applied to classes, fields, and methods to generate the bytecode corresponding to the annotation. For example, if a class is decorated with the @Getter
annotation, Lombok will then generate a getter for each eligible field.
While the actual annotation processing is involved, the following resources can be used to learn more about how Lombok operates under the hood:
- Java 6.0 Features Part – 2: Pluggable Annotation Processing API
- How does Lombok work?
- Project Lombok source code
Working With IDEs
Adding the Lombok dependency to the pom.xml
file suffices to compile and run our application from the command line, but without the Lombok plugin, IDEs will display false errors — such as a missing getter for final fields — for our project. In this section, we will explore how to add the official Lombok IDE plugins to Eclipse and IntelliJ, respectively.
Eclipse
To install the Lombok Eclipse plugin, download the Lombok JAR from MVN Repository (version 1.18.8 at the time of writing) and execute the JAR using the following command:
java -jar lombok-1.18.8.jar
Doing so will start the Lombok installer, which will search for the Eclipse installations on our system. Once the installations have been found, ensure that the installations for which the Lombok plugin should be installed are checked and then click Install / Update
. Doing so will install the Lombok plugin for those Eclipse installations.
Once the plugin is installed for the desired installations, restart Eclipse.
IntelliJ
To install the Lombok IntelliJ plugin, we need to first enable annotation processing in IntelliJ and then install the plugin itself. To enable annotation processing, complete the following steps:
- Click File
- Click Settings
- Click the Build, Execution, Deployment heading
- Click the Compiler heading
- Click Annotation Processors
- Check the Enable annotation processing checkbox in the right-hand panel
- Click the OK button at the bottom of the setting window
To install the plugin, complete the following steps:
- Click File
- Click Settings
- Click the Plugins heading
- Enter
lombok
into the search field - Click the Install button under the
Lombok (Tools integration)
plugin - Click the Accept button at the bottom of the Third-party Plugins Privacy Note window
- Click the Restart IDE button under the
Lombok (Tools integration)
plugin
More Information
For more information on Lombok and how to use its features, see the following:
- The official Lombok Project website
- Introduction to Project Lombok
- Project Lombok: Clean, Concise Java Code
- The Project Lombok Wiki
- How to write less and better code, or Project Lombok
Conclusion
Java is notorious for its verbosity, and the addition of boilerplate code only makes developing code in Java longer in the tooth. To remedy this issue, Lombok introduces various annotations that can be applied to classes and fields to generate methods, such as getters, setters, and constructors. Not only does this reduce the amount of code we must manually write, but it also removes a large portion of the clutter from our class definitions. Additionally, it also ensures that we correctly and consistently implement difficult techniques, such as equality. While Lombok is not as ubiquitous as other frameworks, in the right situations, it can greatly aid in the readability and correctness of Java code.
Opinions expressed by DZone contributors are their own.
Comments