Java Records vs. Lombok: Which Is Best?

Java for a lot of time has been accused and mocked for its verbosity. Even the most passionate Java developers have to admit that it felt ridiculous to declare a bean class with two attributes. If you follow the right recommendations, you end up adding not only getters and setters, but also the implementations of toString hashcode and equals methods. The final result is a chunk of boilerplate that invites you to start learning another language.

import java.util.Objects;

public class Car {

   private String brand;
   private String model;
   private int year;

   public String getBrand() {
      return brand;
   }

   public void setBrand(String brand) {
      this.brand = brand;
   }

   public String getModel() {
      return model;
   }

   public void setModel(String model) {
      this.model = model;
   }

   public int getYear() {
      return year;
   }

   public void setYear(int year) {
      this.year = year;
   }

   @Override
   public String toString() {
      return "Car{" +
              "brand='" + brand + ''' +
              ", model="" + model + "'' +
              ", year=" + year +
              '}';
   }

   @Override
   public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;
      Car car = (Car) o;
      return year == car.year && Objects.equals(brand, car.brand) && Objects.equals(model, car.model);
   }

   @Override
   public int hashCode() {
      return Objects.hash(brand, model, year);
   }
}

Gratefully, libraries like Lombok came to the rescue and reduced the pain in the eyes of a coder. With the new Java Records feature, one may wonder if it’s OK now to retire Lombok library. Let’s do a short analysis.

What is Lombok?

It’s a Java Library highly integrated with development environments that improve (spice) the code via annotations. It’s highly acceptable and used in the Java community.

Using Lombok our Car class looks like this:

import lombok.Data;

@Data
public class Car {

   private String brand;
   private String model;
   private int year;
}

The code is much cleaner and pleasant to the eyes, without losing any functionality from our previous version.

What Is a Java Record?

A Java record can be shortly described as the implementation of the Value Object pattern. It’s a Java class where all its instances are immutable. Hence, all class attributes need to be passed during object creation. It was introduced in Java 14 and it is almost sure that it will continue evolving to improve class design.

The Car class as a record looks like this:

public record Car(String brand, String model, int year) {
   
}

It’s also a huge improvement from our first version. But let’s analyze a few aspects of Lombok and compare them to records to assess if we’re ready to give permanent vacations to our spicy friend.

Immutability

Records are by default immutable. That means that all class attributes are declared implicitly as final. We can say they are like ValueObjects. They don’t have setter methods and all their values ​​need to be passed in the constructor. Lombok can do the same using the @Value annotation, but can also preserve the mutability using just the @Data annotation.

import lombok.Value;

@Value
public class Car {

   private String brand;
   private String model;
   private int year;
}

Bean Convention

Records are not meant to be compliant with bean conventions. The accessor methods are not named with getX and the class does not provide setters nor a no-args constructor. For Lombok on the other hand, simply using the @Data annotation can convert a class into a Java Bean.

Builders

The builder pattern is a great design pattern to improve our object creation syntax. Lombok provides a convenient annotation that implements for us all the boilerplate code of this pattern. Java Records do not intend to provide this implementation (for now).

import lombok.Builder;

@Builder
public class Car {

   private String brand;
   private String model;
   private int year;

   public static void main(String[] args) {
      Car myCamaro = Car.builder()
              .brand("Chevrolet")
              .model("Camaro")
              .year(2022)
              .build();
   }
}

Big Classes

Records look good with a few fields. But add 10 fields to them and you will end up with a monster of a constructor with all the intrinsic problems of big constructors.

public record DetailedCar(
  String brand, String model, int year,
  String engineCode, String engineType, String requiredFuel,
  String fuelSystem, String maxHorsePower, String maxTorque, 
  float fuelCapacity) {
}

DetailedCar camaroDetailed = new DetailedCar(
  "Chevrolet", "Camaro", 2022, "LTG", "Turbocharged",
  "Gas I4", "Direct Injection", "275 @ 560", "295 @ 3000-4500", 
  19.0f);

Using Lombok we could decide to leave the class as a bean having to option to use setters to set the state of the object or use a builder to have a cleaner way to construct the instance. The only caveat is that we could leave the instance in an incomplete state by not forcing all attributes to be set. In the case of the Builder annotation, we can mark all the class attributes as @nonNull making them all required when building it. But this would throw an error at runtime, versus forcing a compilation error with records.

import lombok.Builder;
import lombok.NonNull;

@Builder
public class DetailedCar {
   @NonNull
   private String brand;
   @NonNull
   private String model;
   @NonNull
   private int year;
   @NonNull
   private String engineCode;
   @NonNull
   private String engineType;
   @NonNull
   private String requiredFuel;
   @NonNull
   private String fuelSystem;
   @NonNull
   private String maxHorsePower;
   @NonNull
   private String maxTorque;
   @NonNull
   private float fuelCapacity;

   public static void main(String[] args) {
      DetailedCar camaroIncomplete = DetailedCar.builder()
              .brand("Chevrolet")
              .model("Camaro")
              .year(2022)
              .build();
   }
}

Output:

Exception in thread “main” java.lang.NullPointerException: engineCode is marked non-null but is null

Inheritance

Java record classes, as of this moment, do not support inheritance. You cannot make a record class extend another record class. This could be a limitation to model designing. Although, we all had learned that we should favor composition over inheritance.

Lombok annotations can be parametrized to consider parent class attributes in toString/hashcode/equals methods.

@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Car extends MotorVehicle {

   private String brand;
   private String model;
   private int year;
   
}

Conclusion

Java records are a great new feature of the language, moving in the right direction of cleaner code. We should start using them whenever there is an opportunity. But given the great versatility of Lombok library, and the slow (but surely) pace of Java language changes, it looks like it’s too early to remove the Lombok dependency from our projects.

.

Leave a Comment