Records — Compact Data Carriers, Canonical Constructors, and Components (Java 14+)
In this tutorial, you will learn about Records. We cover key concepts, practical examples, and best practices to help you master this topic.
Java records are transparent data carriers that automatically generate constructors, accessors, equals, hashCode, and toString from component declarations. Records, introduced as a preview in Java 14 and standardized in Java 16, eliminate the boilerplate of writing POJOs — a single line defines the data shape and all common methods follow automatically.
What You'll Learn
- Declaring records and understanding their components
- Compact constructors for validation and normalization
- Custom methods and static members in records
- When records are appropriate and their limitations
Why It Matters
Before records, a simple data class required fields, constructor, getters, equals, hashCode, and toString — typically 50+ lines of boilerplate. Records reduce this to a single line, eliminating bugs from inconsistent equals/hashCode implementations.
Real-World Use
DTOs for REST APIs, event objects in messaging systems, value objects in domain-driven design, and tuple-like returns from methods. Records are now preferred over Lombok's @Data in many modern codebases.
Declaring a Record
public record Point(int x, int y) {}
This single line provides:
- A canonical constructor:
Point(int x, int y) - Accessor methods:
x()andy()(notgetX()/getY()) equals()— compares all componentshashCode()— derived from all componentstoString()— formatted asPoint[x=5, y=3]
Usage:
Point p = new Point(5, 3);
System.out.println(p.x()); // 5
System.out.println(p); // Point[x=5, y=3]
The Canonical Constructor
The constructor generated by the compiler matches the components exactly:
// The compiler generates something like:
public Point(int x, int y) {
this.x = x;
this.y = y;
}
Compact Constructor
You can define a compact constructor to add validation or normalization without repeating the parameter assignments:
public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException("Coordinates must be non-negative");
}
// x and y are automatically assigned to this.x and this.y
}
}
The compact constructor body runs before the implicit field assignments. You can also normalize values:
public record Range(int min, int max) {
public Range {
if (min > max) {
int temp = min;
min = max;
max = temp;
}
}
}
Non-Canonical Constructor
You can add additional constructors that delegate to the canonical one:
public record Point(int x, int y) {
public Point() {
this(0, 0); // delegates to canonical constructor
}
}
Custom Methods
Records can have instance and static methods:
public record Rectangle(double width, double height) {
public double area() {
return width * height;
}
public static Rectangle square(double side) {
return new Rectangle(side, side);
}
}
Limitations
- Records cannot extend other classes (they implicitly extend
java.lang.Record) - Records are implicitly
final— they cannot be abstract - Components are implicitly
private final— there are no setters - Records cannot declare instance fields outside the component list
- Reflection-based frameworks may need special handling (but records work with
--add-opens)
Records and Serialization
Records implement Serializable if they declare they do. The serialized form is based on the component state. Custom readObject/writeObject methods are not allowed — the serialization mechanism uses the canonical constructor.
Records with Annotations
import javax.validation.constraints.NotNull;
public record User(
@NotNull String name,
@NotNull String email
) {}
Annotations on components are propagated to the constructor parameters, accessor methods, and fields.
Local Records
Records can be declared inside methods (local records), which is useful for intermediate data transformations:
public List<String> process(List<Order> orders) {
record OrderSummary(String product, int totalQuantity) {}
return orders.stream()
.collect(Collectors.groupingBy(Order::product,
Collectors.summingInt(Order::quantity)))
.entrySet().stream()
.map(e -> new OrderSummary(e.getKey(), e.getValue()))
.map(OrderSummary::toString)
.toList();
}
Common Mistakes
- Trying to add setters. Records are immutable. You cannot modify a component after construction. To change a value, create a new record instance.
- Forgetting that accessors are
x()notgetX(). Records use the component name directly. Jackson can be configured to recognize them with@JsonAutoDetect. - Using records with mutable components. A record containing a
ListorDateis not deeply immutable — the list can be modified externally. Always use immutable components or defensive copies. - Expecting
finalfields to be writable via reflection. The JVM prevents modification of record components through reflection unlesssetAccessibleis explicitly invoked. - Using records with JPA entities. JPA entities require a no-arg constructor and mutable fields — records are unsuitable. Use records for DTOs and value objects instead.
Practice Questions
1. What methods does a record automatically generate?
Canonical constructor, component accessors, equals(), hashCode(), toString().
2. What is a compact constructor?
A constructor syntax that omits the parameter list — the parameters are the record components. Inside the compact constructor, you can add validation or normalization without repeating assignments.
3. Can a record have instance methods?
Yes. Records can have custom instance and static methods, but only the component list defines the state.
4. Why are records not suitable for JPA entities?
JPA entities require a no-arg constructor, mutable fields, and Lazy Loading proxies — all incompatible with records' immutability and finality.
5. What is the implicit superclass of all records?
java.lang.Record. It is an abstract class that records implicitly extend.
Challenge Question:
Create a BankTransaction record with components amount (double), description (String), and timestamp (LocalDateTime). Add a compact constructor that rejects negative amounts and empty descriptions. Add a static method credit(double amount, String description) that creates a Transaction with a positive amount and the current time. Also add an instance method formatted() that returns a formatted string.
FAQ
Mini Project
Write a program RecordDemo.java that:
- Defines a
Personrecord withname,email,age - Adds a compact constructor that validates email contains
@and age is non-negative - Adds a method
isAdult()that returnsage >= 18 - Adds a static method
fromCsv(String csvLine)that parses a comma-separated line into aPerson - Creates a
List<Person>and demonstrates sorting by age usingComparator.comparing(Person::age) - Shows that two records with the same components are equal
- Attempts to create a record with a
nullname and observes theNullPointerException(if usingObjects.requireNonNull)
What's Next
Records give us transparent data carriers. But what if you need to restrict the set of possible subtypes? Lesson 20 introduces sealed classes (Java 17+) — classes and interfaces that control which other types can extend or implement them.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro