Being lazy with Lombok

lombokjava

Lombok allows us lazy Java developers to stop writing “boilerplate” code in our objects. Through the use of some annotations it will generate the methods for us without the need to write out each getter or setter and many other methods, allowing us to simply add actual functionality to the object or leave it pretty much empty and use it as a data transfer object. This post will go through some of the basics of using Lombok in your code.

Before explaining how to use it I think its best to show you what it can do with a quick example.

Without Lombok

public class PersonDTO {

  private String firstName;
  private String secondName;
  private Date dateOfBirth;
  private String profession;
  private BigDecimal salary;

  public PersonDTO(
      String firstName, String secondName, Date dateOfBirth, String profession, BigDecimal salary) {
    this.firstName = firstName;
    this.secondName = secondName;
    this.dateOfBirth = dateOfBirth;
    this.profession = profession;
    this.salary = salary;
  }

  public PersonDTO() {}

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  public String getSecondName() {
    return secondName;
  }

  public void setSecondName(String secondName) {
    this.secondName = secondName;
  }

  public Date getDateOfBirth() {
    return dateOfBirth;
  }

  public void setDateOfBirth(Date dateOfBirth) {
    this.dateOfBirth = dateOfBirth;
  }

  public String getProfession() {
    return profession;
  }

  public void setProfession(String profession) {
    this.profession = profession;
  }

  public BigDecimal getSalary() {
    return salary;
  }

  public void setSalary(BigDecimal salary) {
    this.salary = salary;
  }
}

With Lombok

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class PersonDTO {

  private String firstName;
  private String secondName;
  private Date dateOfBirth;
  private String profession;
  private BigDecimal salary;
  
}

As you can see it is way way shorter and is still clear what methods it should contain when compared to the original.

One other thing to note before continuing is that it is worth finding a plugin to handle the use of Lombok in your IDE. If you do not have one you are going to see a lot of compiler errors in your IDE, although it will still compile correctly due to the annotations. Intellij, Eclipse and Netbeans all have plugins available for use with Lombok to remove all those scary errors, they can all be found here.

So going back to the example above you can see how the annotations are used to define what methods we want Lombok to generate for us. @Getter creates the getters for each field, @Setter generates the setters, @AllArgsConstructor creates a constructor that takes all the fields as parameters and @NoArgsConstructor makes a empty constructor. I’m sure you get the gist of whats going on by this point.

Now that you have seen a basic example lets look at what else it can do and how to use it. The first thing that we need to do to actually use it is add it to our classpath, which in this post is done via Maven using the following dependency.

<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>

The first annotations I want to look at a bit closer are the @Getter and @Setter annotations. These are prime examples of methods that clutter up classes and are arguably one of the main reasons the word “verbose” is always used to describe Java. So lets get rid of them! If you look back to the code earlier you can see the difference it makes by removing the getters and setters. Using these annotations is straight forward, you either apply it to the class to add them to all fields or mark each individual field with the annotation that you wish to have the respective method.

@AllArgsConstructor
public class PersonDTO {

  @Getter private String firstName;
  @Getter private String secondName;
  @Getter private Date dateOfBirth;
  @Getter @Setter private final String profession;
  @Getter @Setter private BigDecimal salary;

}

The fields in this example all have getters and therefore the @Getter annotation could be applied to the class itself. The @Setter on the other hand has only been added to two fields preventing the others from being changed after initialisation. The annotations come equipped with some parameters to provide configuration. These include value to set the AccessLevel (PUBLIC, MODULE, PROTECTED, PACKAGE, PRIVATE or NONE) and onMethod to include other annotations on the generated methods. @Getter also comes with lazy to control if the method is lazy or not and @Setter also has onParam to add annotations to the parameters of the generated methods.

@ToString and @EqualsAndHashCode do as their annotations suggest and generate the toString, equals and hashCode methods. These are again some pretty basic methods to add to an object and can easily make the class look more cluttered than need be, so rejoice as they have been removed!

Lets have a look at the @ToString annotation. It also comes with a few parameters that allows us to control what exactly the toString method does, so its just like we wrote the method ourselves… but with a bit less typing. By leaving the annotation empty the generated method will include all fields and display their names and values.

@ToString
public class PersonDTO {

  private String firstName;
  private String secondName;
  @JsonFormat(pattern = "dd/MM/yyyy")
  @DateTimeFormat(pattern = "dd/MM/yyyy")
  private Date dateOfBirth;
  private String profession;
  private BigDecimal salary;

}

By invoking the toString method we get the output

PersonDTO(firstName=Joe, secondName=Blogs, dateOfBirth=Sat Apr 29 10:55:21 BST 2017, profession=Computer Scientist, salary=0)

Looks like a toString output to me, but to be sure I auto-generated one in Intellij and it produced.

PersonDTO{firstName='Joe', secondName='Blogs', dateOfBirth=Sat Apr 29 10:57:20 BST 2017, profession='Computer Scientist', salary=0}

So they look a bit different but hopefully that doesn’t bother you at all and if it does then you can write it by hand or use your IDE’s generated method.

As mentioned already we can add some of the available parameters to the annotation to change the output of the generated method.

@ToString(includeFieldNames = false, exclude = {"dateOfBirth", "profession", "salary"})

By changing the annotation to this the field names have been removed and the fields marked in the exclude parameter have been removed from the output.

There are a few extra parameters that can be used including callSuper, doNotUseGetters, of which can include the super in the toString, access the fields directly and only include the chosen fields (the opposite of exclude). The @EqualsAndHashCode annotation is pretty similar so I will skip over its explanation.

Lets look at some of the more interesting annotations. @Builder is something I wish I could have used at work recently as I was writing out a ton of objects with builders and it would have saved me a lot of effort. Again the annotation is pretty clear on what it does, it applies the Builder Pattern to the object.

Without Lombok

public class PersonDTO {

  private String firstName;
  private String secondName;
  private Date dateOfBirth;
  private String profession;
  private BigDecimal salary;

  PersonDTO(
      String firstName, String secondName, Date dateOfBirth, String profession, BigDecimal salary) {
    this.firstName = firstName;
    this.secondName = secondName;
    this.dateOfBirth = dateOfBirth;
    this.profession = profession;
    this.salary = salary;
  }

  public static class Builder {
    private String firstName;
    private String secondName;
    private Date dateOfBirth;
    private String profession;
    private BigDecimal salary;

    public PersonDTOBuilder build() {
      return new PersonDTOBuilder(firstName, secondName, dateOfBirth, profession, salary);
    }

    public Builder firstName(String firstName) {
      this.firstName = firstName;
      return this;
    }

    public Builder secondName(String secondName) {
      this.secondName = secondName;
      return this;
    }

    public Builder dateOfBirth(Date dateOfBirth) {
      this.dateOfBirth = dateOfBirth;
      return this;
    }

    public Builder profession(String profession) {
      this.profession = profession;
      return this;
    }

    public Builder salary(BigDecimal salary) {
      this.salary = salary;
      return this;
    }
  }
}

With Lombok

@Builder
public class PersonDTO {

  private String firstName;
  private String secondName;
  private Date dateOfBirth;
  private String profession;
  private BigDecimal salary;
  
}

Again this is much much shorter than writing out the builder by hand. To produce the original code shown without Lombok I actually used the Delombok feature of the Intellij plugin which converts the byte code that is generated by the Lombok annotations into actual code that you can see in the class. I did change the output slightly to what I thought looked tidy but the majority of the code was created using this feature.

To use the code generated by @Builder we need to write something along the lines of

PersonDTO person = PersonDTO.builder()
            .firstName("Joe")
            .secondName("Blogs")
            .dateOfBirth(new Date())
            .profession("Computer Scientist")
            .salary(BigDecimal.ZERO)
            .build();

Configuration can be applied to the @Builder annotation. builderClassName sets the name of the generated builder class (class_name + “Builder” by default), buildMethodName allows you to choose the name of the build method (called build by default), builderMethodName for the builder instantiation method (builder by default) and toBuilder which adds a method to convert the current objects value back into a builder to be altered again.

Moving onto another annotation, lets look at @NonNull which is placed inside a constructor, null checks the annotated parameter and throws a NullPointerException if required.

Without Lombok

public PersonDTO(
    String firstName, String secondName, Date dateOfBirth, String profession, BigDecimal salary) {
  if (firstName == null) {
    throw new NullPointerException("firstName");
  }
  if (secondName == null) {
    throw new NullPointerException("secondName");
  }
  if (dateOfBirth == null) {
    throw new NullPointerException("dateOfBirth");
  }
  this.firstName = firstName;
  this.secondName = secondName;
  this.dateOfBirth = dateOfBirth;
  this.profession = profession;
  this.salary = salary;
}

With Lombok

public PersonDTO(
    @NonNull String firstName,
    @NonNull String secondName,
    @NonNull Date dateOfBirth,
    String profession,
    BigDecimal salary) {
  this.firstName = firstName;
  this.secondName = secondName;
  this.dateOfBirth = dateOfBirth;
  this.profession = profession;
  this.salary = salary;
}

As you can see this removes the need to write your own null check for each argument you wish to check, which as I have written a few times now makes it look tidier and saves you from writing some extra code… from now on I’m not going to mention these words anymore as I think you get the gist of it.

What if you want to use the @AllArgsConstructor annotation and @NonNull at the same time? To do this you need to add @NonNull onto the field declarations themselves, this will bring the side effect of null checking all Lombok generated methods that set the annotated field. So if you used @Setter in combination with @NonNull on some fields they would be checked before successfully setting the value.

Now that I have mentioned @NonNull we can look at @RequiredArgsConstructor. This annotation will only include fields in the generated constructor that need to have a value set upon initialisation. The fields it includes are marked with final or @NonNull which also incorporates a null check.

Without Lombok

public class PersonDTO {

  @NonNull private String firstName;
  @NonNull private String secondName;
  @NonNull private Date dateOfBirth;
  private final String profession;
  private BigDecimal salary;

  public PersonDTO(String firstName, String secondName, Date dateOfBirth, String profession) {
    this.firstName = firstName;
    this.secondName = secondName;
    this.dateOfBirth = dateOfBirth;
    this.profession = profession;
  }
  
}

With Lombok

@RequiredArgsConstructor
public class PersonDTO {

  @NonNull private String firstName;
  @NonNull private String secondName;
  @NonNull private Date dateOfBirth;
  private final String profession;
  private BigDecimal salary;
  
}

Notice that the salary field has not been marked with final or @NonNull and therefore has been exempt from the equivalent constructor in the example without Lombok.

The last annotation I will cover in this post is @Data which incorporates @ToString, @EqualsAndHashCode, @Getter on all fields, @Setter on all non final fields and @RequiredArgsConstructor. As all of these annotations are included in @Data you will lose the configuration that comes with using them separately. This isn’t really a problem as you can simply add the annotation you wish to configure and it will override the @Data settings for that specific annotation.

@Data
@ToString(includeFieldNames = true, of = {"firstName", "secondName"} )
public class PersonDTO {

  @NonNull private String firstName;
  @NonNull private String secondName;
  @NonNull private Date dateOfBirth;
  final private final String profession;
  private BigDecimal salary;
  
}

In this example all the incorporated annotations will be applied with the toString method being overridden with the parameters inside the @ToString annotation. I haven’t put the without Lombok equivalent as I am sure you don’t want to scroll down a massive code snippet again… and the components that would have made that snippet have already been covered.

That was a pretty long post and there is still plenty more that Lombok can do, for more information simply check their documentation. In conclusion use Lombok to remove a lot the boilerplate code that we all hate writing, giving us more time to do the fun stuff… or time to spend playing on our phones. And maybe, just maybe the word “verbose” will be used a little less to describe Java… but probably not.

If you found this post helpful, please share it and if you want to keep up with my latest posts then you can follow me on Twitter at @LankyDanDev.

Written by Dan Newton
Twitter
LinkedIn
GitHub