Skip to content

Java record class & Lombok

Passing immutable data between objects is very common in Java. With the release of Java 14, you can use records to perform this task.

Very often you write classes that hold data such as query results or service information. In many cases, this data is immutable. Immutability ensures the validity of the data without synchronization.

since the internal state of an immutable object remains constant in time, you can share it among multiple threads.

To accomplish immutability, you must create data classes with the following constraints:

  • private, final field for each piece of data,
  • getter for each field,
  • public constructor with an argument for each field,
  • equals() method that returns true for objects of the same class when all fields match,
  • hashCode() method that returns the same value when all fields match,
  • toString() method that includes the name of the class and the name of each field and its corresponding value

Let’s create a simple Dog class with age and name:

Dog.java
public class Dog {
private final String name;
private final int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (!(obj instanceof Dog)) {
return false;
} else {
Dog other = (Dog) obj;
return Objects.equals(name, other.name)
&& Objects.equals(age, other.age);
}
}
@Override
public String toString() {
return "Dog [name=" + name + ", age=" + age + "]";
}
// getters
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
}

As you can see, there’s a lot of boilerplate code. All the extra code obscures that the class is simply a data class with two fields.

As of JDK 14, you can replace repetitious data classes with records. Records are immutable data classes that require only the type and name of fields.

The equals(), hashCode(), and toString() methods, the private final fields and public constructor are generated by the Java compiler.

Let’s create the same Dog class with age and name:

Dog.java
public record class Dog {String name, int age) {}

Let’s take the Color object as example. It has three integer values representation the RGB channels: red, green, blue. The Color also expose its HEX representation. Moreover, two Color objects are equals if they have the same RGB values.

Color.java
public record Color(int red, int green, int blue) {
public String getHexString() {
return String.format("#%02X%02X%02X", red, green, blue);
}
}

Lombok allows you to create immutable objects using the @Value annotation:

ColorValueObj.java
@Value
public class ColorValueObj {
int red;
int green;
int blue;
public String getHexString() {
return String.format("#%02X%02X%02X", red, green, blue);
}
}

JEP 359 states that records are classes that act as transparent carriers for immutable data. As a result, you can’t stop a record from exposing its member fields.

Lombok allows you to customize the names, access levels, and return types of the getters. Let’s update the ColorValueObj accordingly:

ColorValueObj.java
@Value
@Getter(AccessLevel.NONE)
public class ColorValueObject {
int red;
int green;
int blue;
public String getHexString() {
return String.format("#%02X%02X%02X", red, green, blue);
}
}

Records are a good solution for immutable data objects. If you want to hide the member fields and only expose some operations performed using them, Lombok will be better suited.

Let’s take a look at a record where the data model requires more fields.

EmployeeRecord.java
public record EmployeeRecord(
String firstName,
String lastName,
Long employeeId,
String email,
String phoneNumber,
String address,
int age) {
}

The instantiation of EmployeeRecord could be hard to read, especially if some fields could be left null:

To facilitate these use-cases, Lombok provides an implementation of the Builder design pattern. In order to use it, you need to annotate a class with @Builder:

EmployeeBuilder.java
@Getter
@Builder
public class EmployeeBuilder {
private String firstName;
private String lastName;
private Long employeeId;
private String email;
private String phoneNumber;
private String address;
private int age;
}

Let’s use EmployeeBuilder to create an object with the same attributes:

EmployeeBuilder john = EmployeeBuilder.builder()
.firstName("John")
.lastName("Doe")
.email("john@doe.com")
.age(25)
.build();

Records are better for smaller objects. For objects with many fields, the lack of creation patterns will make Lombok’s @Builder a better option.

You can use java records exclusively for immutable data. If the context requires a mutable java object, you can use Lombok’s @Data object instead.

ColorData.java
@Data
@AllArgsConstructor
public class ColorData {
private int red;
private int green;
private int blue;
public String getHexString() {
return String.format("#%02X%02X%02X", red, green, blue);
}
}

Java records do not support inheritance. They cannot be extended or inherit other classes. On the other hand, Lombok’s @Value objects can extend other classes, but they are final:

MonochromeColor.java
@Value
public class MonochromeColor extends ColorData {
public MonochromeColor(int grayScale) {
super(grayScale, grayScale, grayScale);
}
}

@Data objects can both extend other classes and be extended.