Avoid Lombok. Use Records to write cleaner code in Java 17.

Anurag Rana
Naukri Engineering
Published in
6 min readOct 16, 2023

--

Photo by Luca Bravo on Unsplash

“The ratio of time spent reading vs. writing code is well over 10 to 1. Making it easy to read makes it easier to write.” — Robert C. Martin — Author of Clean Code: A Handbook of Agile Software Craftsmanship

Let’s say you have to define and use a class ‘User’ in your project. Class User is used to hold user data and has fields username, email, and userId.

The traditional way to define this class is as follows.

public class User {
private String username;
private String email;
private int userId;

public User(String username, String email, int userId) {
this.username = username;
this.email = email;
this.userId = userId;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public int getUserId() {
return userId;
}

public void setUserId(int userId) {
this.userId = userId;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

User user = (User) o;

if (userId != user.userId) return false;
if (username != null ? !username.equals(user.username) : user.username != null) return false;
return email != null ? email.equals(user.email) : user.email == null;
}

@Override
public int hashCode() {
int result = username != null ? username.hashCode() : 0;
result = 31 * result + (email != null ? email.hashCode() : 0);
result = 31 * result + userId;
return result;
}

@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", email='" + email + '\'' +
", userId=" + userId +
'}';
}
}

This is a lot of code to hold and use just three values. Approx 65 lines of code.

Lombok to the rescue.

A better way is to use the Lombok library. Lombok reduces the boilerplate code associated with Java classes. A single annotation above the class name can reduce the 65 lines of code to 5 lines of code. Here is an example.

import lombok.Data;

@Data
public class User {
private String username;
private String email;
private int userId;
}

@Data annotation will automatically generate all getter functions, all setter functions for the fields, toString function, constructor, hashCode, and equals function.

The @Data annotation combines several other Lombok annotations, such as @Getter, @Setter, @EqualsAndHashCode, and @toString. We can use these annotations individually if required.

Issues with Lombok:

There are multiple reasons why one should avoid Lombok. Two main reasons in my opinion are:

  1. Third-party dependency: We as developers are relying on a third-party library for such trivial things. Lombok relies on community support for the library’s maintenance. If there are compatibility issues with newer Java versions or if the library becomes unsupported, it can lead to problems in your codebase.
  2. IDE Compatibility: Lombok relies on code generation at compile time, which may not always work seamlessly with all Integrated Development Environments (IDEs). Some IDEs may not fully support Lombok features, making it difficult to navigate and understand the generated code.

Do we have a better alternative?

Yes. We can start using Records.

What is a Record?

Records are a new feature introduced in Java starting from Java 14 (as a preview feature) and officially in Java 16. Records provide a concise way to define simple classes primarily intended for encapsulating data. They are a type of class that automatically generates common methods such as constructors, equals(), hashCode(), and toString() based on the class's fields.

Do you see the similarity between Record and Lombok? They both are helping us achieve the same thing.

How to use Records?

To define the above-discussed User class using Records, we need to do this.

public record UserRecord(String username, String email, int userId) {
}

Thats it. Just one line of code to achieve what we did with 65 lines of traditional coding and 5 lines of Lombok. Plus we do not have to be dependent on a third-party library.

Once we create the above class, Java internally defines three final variables and their getter methods in addition to the class-level methods such as toString, hashCode, and equals.

Let's discuss Records in detail.

Once we have the User record class in place, we can start using it.

// Initialize the record.
UserRecord userRecord = new UserRecord("rana", "rana@yopmail.com", 1234);
// get the properties
System.out.println(userRecord.email());
System.out.println(userRecord.toString());

Mind it, there is no ‘get’ keyword with getter methods. We need to directly use the variable name as the method name. For example, instead of getEmail() as we use traditionally, we just use email() while calling Record methods.

We can not set the value of a property of Record once initialized. All the variables are final. This means Records are immutable.

We can define instance and class functions in a Record. We can define static variables. We can not define instance variables.

  // class (static) variable
public static final String invalidEmailMessage = "INVALID EMAIL";

// instance variable - not allowed. Will throw Error.
public String defaultEmail = "unknown@yopmail.com";

// Class function
public static void sayMyName() {
System.out.println("Heisenberg");
}

// instance function
public String emailDomain() {
return this.email.split("@")[1];
}
// calling instance method using object
userRecord.emailDomain();
// calling static method using Class.
UserRecord.sayMyName();

Record classes can not be extended. All Records class implicitly extends the Record class. And Java doesn’t allow multiple inheritance. Hence our Record class can not be the child of any other class.

Records are also by default final. Hence we can not use them as parent class of any other class.

Record Constructor

The record declares a default constructor with all the parameters. This type of constructor is known as a canonical constructor.

public UserRecord(String username, String email, int userId) {
this.username = username;
this.email = email;
this.userId = userId;
}

We can write custom logic in constructors.

public UserRecord(String username, String email, int userId) {
this.username = username;
this.email = email;
this.userId = userId;
if (userId < 1) {
throw new IllegalArgumentException("UserId can not be less than 1");
}
}

There is an awesome feature where we can create a compact constructor by eliminating unnecessary details. For example, the above canonical constructor with custom logic can be re-written in compact form as:

public UserRecord {
if (userId < 1) {
throw new IllegalArgumentException("UserId can not be less than 1");
}
}

Quick comparison:

+------------------------+-------------------------------+----------------+
| Feature | Lombok | Records |
+------------------------+-------------------------------+----------------+
| Immutability | No | Yes |
| Extensibility | Yes | No |
| Boilerplate code | Reduces | Reduces |
| Readability | Can be more difficult to read | Easier to read |
| Robustness | Less robust | More robust |
| Third-Party dependency | Yes | No |
| IDE compatibility | Not easy | Easy |
+------------------------+-------------------------------+----------------+

Is there any performance difference?

No. In terms of performance, there is no significant difference between using Java records and Lombok annotations. Both generate code that, once compiled, is no different from hand-written code in terms of performance characteristics. The generated code is optimized by the Java compiler, so there is little to no performance overhead.

Conclusion:

This article shows that we should use the Records to write cleaner and more readable code. Records help us in reducing the boilerplate code without any third-party library. Lombok poses some compatibility issues with IDEs.

We should use Lombok only if we are working on older Java versions or if we want to write extendible and mutable classes. Otherwise, for most of the purposes, Records are always a better choice.

--

--