Lombok @Data vs Java 14 Records: Key Differences for Data Classes
Lombok @Data generates mutable POJOs with JavaBean-style accessors via compile-time annotation processing, while Java 14 records provide immutable data carriers with component accessors as a native language feature.
When eliminating boilerplate in data-centric Java classes, developers must choose between Project Lombok's @Data annotation and the native record feature introduced in Java 14. Both approaches automatically generate equals(), hashCode(), toString(), and constructors, yet they differ fundamentally in mutability, inheritance models, and implementation mechanisms. According to the JetBrains/kotlin documentation on Java interoperability, these distinctions become critical when integrating Kotlin with existing Lombok-generated classes or modern Java records.
Core Architecture: Annotation Processing vs. Language Feature
Lombok @Data Implementation
In lombok/Data.java, the @Data annotation serves as a meta-annotation that aggregates @Getter, @Setter, @RequiredArgsConstructor, @ToString, and @EqualsAndHashCode. The actual bytecode generation occurs in lombok/processor/HandlerData.java, where the annotation processor injects synthetic methods into the compiled class file. This approach produces standard JavaBean patterns including mutable setters for non-final fields.
Java Record Implementation
As defined in java/lang/Record.java from the OpenJDK source, records are a sealed abstract class in the Java language. When you declare public record Person(String name, int age), the compiler—following the Java Language Specification §8.10—automatically generates a final class extending java.lang.Record with final fields, a canonical constructor, and component accessor methods. No external annotation processor is required.
Generated Methods: Detailed Comparison
Accessor Patterns
Lombok @Data generates traditional JavaBean accessors: getX() and setX(...) methods for each field. For a field named name, it produces public String getName() and public void setName(String name).
Java records generate component accessors that match the component name exactly: public String name() and public int age(). Additionally, the compiler generates componentN() methods (e.g., component1(), component2()) for positional access, though these are primarily intended for framework use rather than direct application code.
Mutability and Setters
Critical distinction: Lombok generates setters for all non-final fields by default, making @Data classes mutable unless you explicitly mark fields as final or use @Value instead.
Records enforce immutability—all components are implicitly final, and no setters are generated. To "modify" a record, you must implement a copy method manually (similar to Lombok's @With functionality):
public record Person(String name, int age) {
public Person withAge(int newAge) {
return new Person(this.name, newAge);
}
}
Equality and Hashing
Both approaches generate equals() and hashCode() implementations that consider all fields/components. However, Lombok's implementation in HandlerData.java includes a canEqual method to handle inheritance hierarchies correctly, while records generate a straightforward comparison based strictly on the component values without inheritance considerations (since records are final).
Inheritance and Extension Limitations
Lombok Class Hierarchy
Classes annotated with @Data can extend other non-final classes and can be extended themselves (unless declared final). This flexibility comes with a caveat: Lombok's generated equals and hashCode methods use canEqual to ensure symmetric equality when inheritance is involved, as implemented in the annotation processor.
Record Sealing Constraints
Records are implicitly final and cannot extend any class other than java.lang.Record. They cannot be subclassed, and they cannot declare instance fields beyond those defined in the record header. This design enforces a strict "transparent carrier for immutable data" pattern, as specified in JLS §8.10.
Practical Implementation Examples
The following examples demonstrate the actual generated code based on the respective source implementations.
Lombok @Data Class
// Source annotation: lombok/Data.java
// Processing: lombok/processor/HandlerData.java
@Data
public class Person {
private String name;
private int age;
}
Generated bytecode structure:
public class Person {
private String name;
private int age;
// Required-args constructor (implicit no-arg if no other constructors)
public Person() {}
// JavaBean accessors
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
public int getAge() { return this.age; }
public void setAge(int age) { this.age = age; }
// Structural methods
@Override public boolean equals(Object o) { /* uses canEqual */ }
@Override public int hashCode() { /* based on fields */ }
@Override public String toString() { /* field values */ }
protected boolean canEqual(Object other) { /* inheritance support */ }
}
Java 14+ Record
// Base class: java/lang/Record.java (OpenJDK)
// Specification: JLS §8.10
public record Person(String name, int age) { }
Generated members:
public final class Person extends java.lang.Record {
private final String name;
private final int age;
// Canonical constructor
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// Component accessors
public String name() { return this.name; }
public int age() { return this.age; }
// Positional accessors (primarily for frameworks)
public String component1() { return this.name; }
public int component2() { return this.age; }
// Structural methods
@Override public boolean equals(Object o) { /* component comparison */ }
@Override public int hashCode() { /* component-based */ }
@Override public String toString() { /* component values */ }
}
Summary
- Mutability: Lombok
@Datagenerates mutable classes with setters for non-final fields, while Java records enforce immutability with final components and no setters. - Accessor style: Lombok uses JavaBean
getX()/setX()conventions; records use canonicalx()accessors andcomponentN()methods. - Implementation mechanism: Lombok relies on compile-time annotation processing via
lombok/processor/HandlerData.java; records are native compiler features defined in JLS §8.10. - Inheritance: Lombok classes support class hierarchies with
canEqualfor equality; records are implicitly final and extend onlyjava.lang.Record. - IDE support: Records work natively in all Java 14+ IDEs; Lombok requires a specific plugin for navigation and refactoring of generated members.
Frequently Asked Questions
Can I use Lombok annotations with Java records?
No, you cannot apply Lombok's @Data or other field-level annotations to record components in a meaningful way because records are final immutable classes with fixed component definitions. Records already provide the boilerplate elimination that Lombok targets, and applying Lombok to records often causes compilation conflicts or redundant processing since the compiler already generates the standard methods.
Do Java records generate setters like Lombok @Data?
No, Java records do not generate setters. All record components are implicitly final, making records immutable by design. To update data in a record, you must manually implement a copy method that returns a new instance with modified values, similar to Lombok's @With functionality but requiring explicit implementation in the record body.
Which approach offers better IDE and tooling support?
Java records offer superior IDE support because they are a native language feature since Java 14. Any standard Java IDE provides full navigation, refactoring, and code completion for records without additional plugins. Lombok requires a specific IDE plugin (such as the Lombok IntelliJ plugin) to recognize generated methods like getName() or equals() for proper navigation and refactoring operations.
Can Kotlin interoperate with both Lombok @Data classes and Java records?
Yes, according to the JetBrains/kotlin documentation on Java interoperability, Kotlin can consume both Lombok-generated classes and Java records. However, Kotlin treats them differently: Lombok classes appear as standard Java classes with synthetic methods, while records are recognized as Kotlin data-like classes with component functions mapped to the record's accessor methods. When using Lombok with Kotlin, ensure the Lombok plugin is configured correctly to generate bytecode before Kotlin compilation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →