How To Reduce Boilerplate Code In Java Using Lombok
Suresh Kumar Mahto
Technical Architect l AWS Certified Architect | Hi-tech Engineering
What is Lombok?
It is an open source Java API which automates the creation of boilerplate code like getter(), setter(), equals(), hashCode(), toString(), constructors etc. and reduces the time spent in writing such kind of codes just by adding annotations to your classes & its members. For example, suppose you have a class and you don’t want to implement its toString() method manually in your class. Then all you have to do is : add the @ToString at the top of your class. Simultaneously, the Lombok Project will use the fields of your class to implement the toString() method behind the scenes. Now, what will happen behind the scene? The implementation of toString() method will exist in compiled (.class) file but you can’t see it in your source (.java) file. Furthermore, Lombok modifies the source code before sending it to the compiler behind the scene.
What are the benefits of using Lombok ?
You might think that if we can generate code automatically with the help of an IDE ( IntelliJ/Eclipse/STS etc.), then what will be the benefit of using Lombok? For example, suppose you have a POJO class containing 10 fields/properties. In the middle of development if you want to change number of fields, or name of field or datatype of field. Then you must either update the?implementation of getterXXX(), setterXXX(), toString(), equals(), hashCode(), parameterized constructors etc. or remove them & regenerate once again. But if you use Lombok library, you don’t need to make any changes in mentioned methods or parameterized constructors. Lombok will take care of it and automatically update the changes in .class file.
Moreover, if you remove any field from the class, Lombok will remove the same from .class file. In summary, you have to just declare the fields in your class and apply the annotations accordingly.
How To Reduce Boilerplate Code In Java Using Lombok
Now let’s get into our topic "How to reduce boilerplate code in Java using Lombok". I'm going to explain common annotations which are widely used across most of the projects.
@Getter
@Getter creates getter methods for members of a class. However, it can be used at class level & field level. Also, if applied at the class level, will generate getter methods for all fields. But if applied at field level, will generate getter of the same field only. For boolean fields it adds ‘is’ prefix in place of ‘get’.
//How to define the Getter and Setter annotation on class
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
@Getter
public class Book {
private String id;
private String title;
@Setter(AccessLevel.PRIVATE)
double Price;
@Setter(AccessLevel.PROTECTED)
private String author;
@Setter(AccessLevel.PUBLIC)
private boolean available;
}
//How class looks like on above implemenation
public class Book
{
private String id;
private String title;
double price;
private String author;
private boolean available;
private double getPrice() {
return this.price;
}
protected void setAuthor(final String author) {
this.author = author;
}
public void setAvailable(final boolean available) {
this.available = available;
}
public String getId() {
return this.id;
}
public String getTitle() {
return this.title;
}
public String getAuthor() {
return this.author;
}
public boolean isAvailable() {
return this.available;
}
}
@Setter
@Setter creates setter methods for members of a class. However, it can be used at class level & field level. Also, if applied at the class level, will generate setter methods for all fields. But if applied at field level, will generate setter of the same field only. Example is provided above along with Getter.
@ToString
@ToString creates an implementation of toString() method. By default It will not include any static field in the implementation of toString(). Further, we can exclude the specific field in the implemntation of toString() by using optional parameter ‘exclude’. We can also include the output of the toString() method of super class by setting optional parameter ‘callSuper’ to true.
//How to use @ToString on java class
import lombok.ToString;
@ToString(callSuper=true,exclude="author")
public class Book {
private static String id;
private String title;
private double price;
private String author;
private boolean available;
}
//How class looks like on above implemenation
public class Book
{
private static String id;
private String title;
private double price;
private String author;
private boolean available;
@Override
public String toString() {
return "Book(super=" + super.toString() + ", title=" + this.title + ",
price=" + this.price + ", available=" + this.available + ")";
}
}
@EqualsAndHashCode
This annotation creates both equals() and hashCode() methods as both are tied together by the hashCode contract. By default both methods will include all fields except which are static and transient. Like @ToString ‘exclude’ parameter prevents a field from being included in the implemented methods. Also like @ToString we can optionally include ‘callSuper’ parameter.
//How to use @EqualsAndHashCode on java class
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(exclude={"id"})
public class Book {
private String id;
private String title;
private Double price;
private String author;
private Boolean available;
private static String publisher;
private transient String ssn;
}
//How converted class looks like for above implemenations
public class Book
{
private String id;
private String title;
private Double price;
private String author;
private Boolean available;
private static String publisher;
private transient String ssn;
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Book)) {
return false;
}
final Book other = (Book)o;
if (!other.canEqual(this)) {
return false;
}
final Object this$title = this.title;
final Object other$title = other.title;
Label_0065: {
if (this$title == null) {
if (other$title == null) {
break Label_0065;
}
}
else if (this$title.equals(other$title)) {
break Label_0065;
}
return false;
}
final Object this$price = this.price;
final Object other$price = other.price;
Label_0102: {
if (this$price == null) {
if (other$price == null) {
break Label_0102;
}
}
else if (this$price.equals(other$price)) {
break Label_0102;
}
return false;
}
final Object this$author = this.author;
final Object other$author = other.author;
Label_0139: {
if (this$author == null) {
if (other$author == null) {
break Label_0139;
}
}
else if (this$author.equals(other$author)) {
break Label_0139;
}
return false;
}
final Object this$available = this.available;
final Object other$available = other.available;
if (this$available == null) {
if (other$available == null) {
return true;
}
}
else if (this$available.equals(other$available)) {
return true;
}
return false;
}
protected boolean canEqual(final Object other) {
return other instanceof Book;
}
@Override
public int hashCode() {
final int PRIME = 59;
int result = 1;
final Object $title = this.title;
result = result * 59 + (($title == null) ? 43 : $title.hashCode());
final Object $price = this.price;
result = result * 59 + (($price == null) ? 43 : $price.hashCode());
final Object $author = this.author;
result = result * 59 + (($author == null) ? 43 : $author.hashCode());
final Object $available = this.available;
result = result * 59 + (($available == null) ? 43 : $available.hashCode());
return result;
}
}
@NonNull
When the @NonNull annotation is placed before a method parameter, or constructor 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. In fact, by using this annotation, you avoid having to manually check and to manually raise NullPointerException. Generally, this annotation is used by @RequiredArgsConstructor to indicate that variable is selected for parameterized constructor creation.
//How to use @NonNull on java class
import lombok.NonNull;
import lombok.Setter;
@Setter
public class Book {
private String id;
@NonNull
private String title;
@NonNull
private Double price;
@NonNull
private String author;
private Boolean available;
}
//How converted class looks like for above implemenations
import lombok.NonNull;
public class Book
{
private String id;
@NonNull
private String title;
@NonNull
private Double price;
@NonNull
private String author;
private Boolean available;
public void setId(final String id) {
this.id = id;
}
public void setTitle(@NonNull final String title) {
if (title == null) {
throw new NullPointerException("title is marked non-null but is null");
}
this.title = title;
}
public void setPrice(@NonNull final Double price) {
if (price == null) {
throw new NullPointerException("price is marked non-null but is null");
}
this.price = price;
}
public void setAuthor(@NonNull final String author) {
if (author == null) {
throw new NullPointerException("author is marked non-null but is null");
}
this.author = author;
}
public void setAvailable(final Boolean available) {
this.available = available;
}
}
@RequiredArgsConstructor
We use this annotation to provide selected fields as parameters inside constructor i.e. Parameterized constructor. Also Field should be selected using @NonNull annotation. Furthermore, if no field has this annotation, then zero field selected and it generates the default constructor only. This will also include fields in the constructor creation which are declared final.
//How to use @RequiredArgsConstructoron annotation in java class
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class Book {
private String id;
@NonNull
private String title;
@NonNull
private Double price;
private final String author;
private Boolean available;
}
//How converted class looks like on above implemenations
import lombok.NonNull;
public class Book
{
private String id;
@NonNull
private String title;
@NonNull
private Double price;
private final String author;
private Boolean available;
public Book(@NonNull final String title, @NonNull final Double price, final String author) {
if (title == null) {
throw new NullPointerException("title is marked non-null but is null");
}
if (price == null) {
throw new NullPointerException("price is marked non-null but is null");
}
this.title = title;
this.price = price;
this.author = author;
}
}
@Data
This annotation is a combination of the @ToString, @EqualsAndHashCode, @Getter, @Setter and @RequiredArgsConstructor annotations.
领英推荐
//How to use @Data annotation in java class
import lombok.Data;
import lombok.NonNull;
@Data
public class Book {
private String id;
@NonNull
private String title;
@NonNull
private Double price;
private String author;
private Boolean available;
}
//How converted class looks like on above implemenations
import lombok.NonNull;
public class Book
{
private String id;
@NonNull
private String title;
@NonNull
private Double price;
private String author;
private Boolean available;
public String getId() {
return this.id;
}
@NonNull
public String getTitle() {
return this.title;
}
@NonNull
public Double getPrice() {
return this.price;
}
public String getAuthor() {
return this.author;
}
public Boolean getAvailable() {
return this.available;
}
public void setId(final String id) {
this.id = id;
}
public void setTitle(@NonNull final String title) {
if (title == null) {
throw new NullPointerException("title is marked non-null but is null");
}
this.title = title;
}
public void setPrice(@NonNull final Double price) {
if (price == null) {
throw new NullPointerException("price is marked non-null but is null");
}
this.price = price;
}
public void setAuthor(final String author) {
this.author = author;
}
public void setAvailable(final Boolean available) {
this.available = available;
}
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (!(o instanceof Book)) {
return false;
}
final Book other = (Book)o;
if (!other.canEqual(this)) {
return false;
}
final Object this$id = this.getId();
final Object other$id = other.getId();
Label_0065: {
if (this$id == null) {
if (other$id == null) {
break Label_0065;
}
}
else if (this$id.equals(other$id)) {
break Label_0065;
}
return false;
}
final Object this$title = this.getTitle();
final Object other$title = other.getTitle();
Label_0102: {
if (this$title == null) {
if (other$title == null) {
break Label_0102;
}
}
else if (this$title.equals(other$title)) {
break Label_0102;
}
return false;
}
final Object this$price = this.getPrice();
final Object other$price = other.getPrice();
Label_0139: {
if (this$price == null) {
if (other$price == null) {
break Label_0139;
}
}
else if (this$price.equals(other$price)) {
break Label_0139;
}
return false;
}
final Object this$author = this.getAuthor();
final Object other$author = other.getAuthor();
Label_0176: {
if (this$author == null) {
if (other$author == null) {
break Label_0176;
}
}
else if (this$author.equals(other$author)) {
break Label_0176;
}
return false;
}
final Object this$available = this.getAvailable();
final Object other$available = other.getAvailable();
if (this$available == null) {
if (other$available == null) {
return true;
}
}
else if (this$available.equals(other$available)) {
return true;
}
return false;
}
protected boolean canEqual(final Object other) {
return other instanceof Book;
}
@Override
public int hashCode() {
final int PRIME = 59;
int result = 1;
final Object $id = this.getId();
result = result * 59 + (($id == null) ? 43 : $id.hashCode());
final Object $title = this.getTitle();
result = result * 59 + (($title == null) ? 43 : $title.hashCode());
final Object $price = this.getPrice();
result = result * 59 + (($price == null) ? 43 : $price.hashCode());
final Object $author = this.getAuthor();
result = result * 59 + (($author == null) ? 43 : $author.hashCode());
final Object $available = this.getAvailable();
result = result * 59 + (($available == null) ? 43 : $available.hashCode());
return result;
}
@Override
public String toString() {
return "Book(id=" + this.getId() + ", title=" + this.getTitle() + ", price=" + this.getPrice() + ", author=" + this.getAuthor() + ", available=" + this.getAvailable() + ")";
}
public Book(@NonNull final String title, @NonNull final Double price) {
if (title == null) {
throw new NullPointerException("title is marked non-null but is null");
}
if (price == null) {
throw new NullPointerException("price is marked non-null but is null");
}
this.title = title;
this.price = price;
}
}
@NoArgsConstructor
This annotation generates Default constructor for our class.
//How to use @NoArgsConstructor annotation in java class
import lombok.NoArgsConstructor;
@NoArgsConstructor
public class Book {
private String id;
private String title;
private Double price;
private String author;
private Boolean available;
}
//How converted class looks like on above implemenations
public class Book
{
private String id;
private String title;
private Double price;
private String author;
private Boolean available;
public Book() {
}
}
@AllArgsConstructor
This annotation generates all param constructor for our class. In addition, it generates default constructor if no fields found. If field exists then parameterized constructor is generated.
//How to use @AllArgsConstructor annotation in java class
import lombok.NoArgsConstructor;
@AllArgsConstructor
public class Book {
private String id;
private String title;
private Double price;
private String author;
private Boolean available;
}
//How converted class looks like on above implemenations
public class Book { private String id;
private String title;
private Double price;
private String author;
private Boolean available;
public Book(String id, String title, Double price, String author, Boolean available) {
this.id = id;
this.title = title;
this.price = price;
this.author = author;
this.available = available;
}
}
@Cleanup
It will tell Project Lombok to automatically close this resource once it is no longer in use in the current scope like FileInputStream. It ensures the variable declaration that you annotate will be cleaned up by calling its close method, regardless of what happens.
//How to use @Cleanup annotation in java class
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import lombok.Cleanup;
public class CleanupTest{
public void test() throws FileNotFoundException, IOException {
@Cleanup
InputStream in = new FileInputStream("input.txt");
@Cleanup
OutputStream out = new FileOutputStream("output.txt");
}
}
//How converted class looks like on above implemenations
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.List;
public class CleanupTest
{
public CleanupTest() {}
public void test() throws FileNotFoundException, java.io.IOException
{
InputStream in = new FileInputStream("input.txt");
try
{
OutputStream out = new FileOutputStream("output.txt");
if (Collections.singletonList(out).get(0) != null) out.close();
}
finally
{
if (Collections.singletonList(in).get(0) != null) in.close();
}
}
}
@Builder
This annotation when used with a class creates Builder for all instance fields available in the class.
//How to use @Builder annotation in java class
import lombok.Builder;
@Builder
public class Book {
private String id;
private String title;
private Double price;
private final String author;
private Boolean available;
}
//How converted class looks like on above implemenations
public class Book
{
private String id;
private String title;
private Double price;
private final String author;
private Boolean available;
Book(final String id, final String title, final Double price, final String author, final Boolean available) {
this.id = id;
this.title = title;
this.price = price;
this.author = author;
this.available = available;
}
public static Book.BookBuilder builder() {
return new Book.BookBuilder();
}
}
@Builder on Constructors: If you want to create Builder for some specific fields, create a parameterized constructor using those fields and apply @Builder on that constructor.
@Builder on Methods: Like constructor, If you want to create Builder for some specific fields, create a parameterized method using those fields and apply @Builder to that method.
@Synchronized
However, @Synchronized is a safer variant of the synchronized method modifier. Like synchronized, the annotation can be used on static and instance methods only. It operates similarly to the synchronized keyword, but it locks on different objects. The keyword locks on this, but the annotation locks on a field named $lock, which is private.
If the field does not exist, it is created for you. If you annotate a static method, the annotation locks on a static field named $LOCK instead.
//How to use @Synchronized annotation in java class
import lombok.Synchronized;
public class SynchronizedExample {
private final Object readLock = new Object();
@Synchronized
public static void hello() {
System.out.println("world");
}
@Synchronized
public int answerToLife() {
return 42;
}
@Synchronized("readLock")
public void foo() {
System.out.println("bar");
}
}
//How converted class looks like on above implemenations
public class SynchronizedExample {
private static final Object $LOCK = new Object[0];
private final Object $lock = new Object[0];
private final Object readLock = new Object();
public static void hello() {
Object object = $LOCK;
synchronized (object) {
System.out.println("world");
}
}
public int answerToLife() {
Object object = this.$lock;
synchronized (object) {
return 42;
}
}
public void foo() {
Object object = this.readLock;
synchronized (object) {
System.out.println("bar");
}
}
}
@SneakyThrows
In case of a Java program with exceptions, Java requires that we must either declare or handle a checked exception. Otherwise, the code will show us compilation errors. If we use @SneakyThrows, we don’t have to either declare or handle checked exceptions. Now let’s consider following program which will force us to declare or handle ArithmeticException & ArrayIndexOutOfBoundsException. In fact, if we use @SneakyThrows, we don’t get any compilation errors.
//How to use @SneakyThrows annotation in java class
import lombok.SneakyThrows;
public class SneakyThrowsTest {
@SneakyThrows({ ArithmeticException.class, ArrayIndexOutOfBoundsException.class })
public static void test() {
int a[] = new int[4];
a[4] = 24 / 0;
}
}
//How converted class looks like on above implemenations
public class SneakyThrowsTest
{
public static void test() {
try {
try {
final int[] a = new int[4];
a[4] = 24 / 0;
}
catch (ArithmeticException $ex) {
throw $ex;
}
}
catch (ArrayIndexOutOfBoundsException $ex2) {
throw $ex2;
}
}
}
Logging Annotations
@CommonsLog, @Slf4j, @Log4j, @Log4j2, @Flogger, @Log, @XSlf4j, @JBossLog
There are various Logging Annotations. When applied on the class, they will create a Logger instance as ‘log’ that can be used further in methods. Although we will do this exercise with different Logging libraries. Also, make sure you have included jars as dependency provided in comments of each class mentioned in the below code snippet accordingly. However, we will test all of them in a single class. Subsequently, you are supposed to create a separate class as a particular library you need to implement.
//How to use Logging annotation in java class
import lombok.extern.apachecommons.CommonsLog;
import lombok.extern.flogger.Flogger;
import lombok.extern.java.Log;
import lombok.extern.jbosslog.JBossLog;
import lombok.extern.log4j.Log4j;
import lombok.extern.log4j.Log4j2;
import lombok.extern.slf4j.Slf4j;
import lombok.extern.slf4j.XSlf4j;
public class LoggerTest{
}
@CommonsLog
class CommonsLogTest { // dependency:commons-logging
}
@Slf4j
class Slf4jLogTest { // dependency:logback-classic
}
@Log4j
class Log4jLogTest { // dependency:log4j
}
@Log4j2
class Log4j2LogTest { // dependency:log4j-api, log4j-core
}
@Flogger
class FloggerLogTest { // dependency:flogger, flogger-system-backend
}
@Log
class LogTest {
}
@XSlf4j
class XSlf4jLogTest { // dependency:slf4j-ext
}
@JBossLog
class JBossLogLogTest { // dependency:jboss-logging
}
//How converted class looks like on above implemenations
import org.apache.commons.logging.LogFactory
import org.apache.commons.logging.Log;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import org.apache.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.google.common.flogger.FluentLogger;
import java.util.logging.Logger;
import org.slf4j.ext.XLoggerFactory;
import org.slf4j.ext.XLogger;
import org.jboss.logging.Logger;
public class LoggerTest
{
}
class CommonsLogTest
{
private static final Log log;
static {
log = LogFactory.getLog((Class)CommonsLogTest.class);
}
}
class Slf4jLogTest
{
private static final Logger log;
static {
log = LoggerFactory.getLogger((Class)Slf4jLogTest.class);
}
}
class Log4jLogTest
{
private static final Logger log;
static {
log = Logger.getLogger((Class)Log4jLogTest.class);
}
}
class Log4j2LogTest
{
private static final Logger log;
static {
log = LogManager.getLogger((Class)Log4j2LogTest.class);
}
}
class FloggerLogTest
{
private static final FluentLogger log;
static {
log = FluentLogger.forEnclosingClass();
}
}
class LogTest
{
private static final Logger log;
static {
log = Logger.getLogger(LogTest.class.getName());
}
}
class XSlf4jLogTest
{
private static final XLogger log;
static {
log = XLoggerFactory.getXLogger((Class)XSlf4jLogTest.class);
}
}
class JBossLogLogTest
{
private static final Logger log;
static {
log = Logger.getLogger((Class)JBossLogLogTest.class);
}
};