Records in Java
Introduction
For decades, writing a simple class in Java just to hold data required an excessive amount of boilerplate. Developers had to write private fields, constructors, getters, equals(), hashCode(), and toString() methods. While IDEs could auto-generate this code, it still cluttered the codebase and obscured the class's actual purpose.
Introduced as a preview feature in Java 14 and made permanent in Java 16, Records solve this exact problem. Records are a restricted form of a class designed specifically to be transparent carriers for immutable data.
This guide will break down the syntax, semantics, and underlying mechanics of Java Records.
Core Concept: What are Records?
A Record provides a compact syntax for declaring classes that are transparent carriers for shallowly immutable data. When you declare a record, the Java compiler automatically generates:
- A private, final field for each component of the record.
- A public read accessor method for each component (named exactly like the component, e.g.,
name()instead ofgetName()). - A public constructor whose signature matches the record's components.
- Robust implementations of
equals(),hashCode(), andtoString().
Why Use Records?
- Conciseness: Drastically reduces boilerplate.
- Immutability: Encourages functional programming paradigms and thread safety.
- Intent: Clearly communicates to other developers that the class is just a data carrier.
Code Examples
1. Beginner Example: The Basics
Let's look at how a traditional POJO (Plain Old Java Object) compares to a Record.
The Old Way:
public class PointClass {
private final int x;
private final int y;
public PointClass(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
@Override
public boolean equals(Object o) { /* ... */ }
@Override
public int hashCode() { /* ... */ }
@Override
public String toString() { /* ... */ }
}
The Record Way:
public record Point(int x, int y) {}
That single line of code achieves everything the verbose class did.
public class BasicExample {
public static void main(String[] args) {
Point p1 = new Point(10, 20);
Point p2 = new Point(10, 20);
System.out.println(p1.x()); // Accessor method (not getX())
System.out.println(p1); // Output: Point[x=10, y=20]
System.out.println(p1.equals(p2)); // true
}
}
2. Intermediate Example: Compact Constructors
Records allow you to define a "compact constructor" to validate or normalize data without redefining the parameter list.
public record User(String username, int age) {
// Compact Constructor
public User {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
if (username == null || username.isBlank()) {
throw new IllegalArgumentException("Username is required");
}
// Normalization
username = username.trim().toLowerCase();
// The assignment to the fields (this.username = username) is implicit!
}
}
3. Advanced Example: Implementing Interfaces
While Records cannot extend other classes (they implicitly extend java.lang.Record), they can implement interfaces. This is highly useful for defining data shapes that conform to a specific contract.
interface Shape {
double area();
}
public record Circle(double radius) implements Shape {
public Circle {
if (radius <= 0) throw new IllegalArgumentException("Radius must be positive");
}
@Override
public double area() {
return Math.PI * radius * radius;
}
}
public record Rectangle(double width, double height) implements Shape {
@Override
public double area() {
return width * height;
}
}
Restrictions and Under the Hood
Restrictions
- A record class is implicitly
final. It cannot be subclassed. - It implicitly extends
java.lang.Recordand therefore cannot extend any other class. - All instance fields are implicitly
private final. You cannot add non-final instance fields. - You can, however, add static fields, static methods, and instance methods.
How it works in Bytecode
When you compile a record, javac emits a class that extends java.lang.Record. The equals, hashCode, and toString methods are implemented using InvokeDynamic and a bootstrap method in java.lang.runtime.ObjectMethods. This allows the JVM to optimize these methods dynamically at runtime, providing better performance and smaller bytecode than static generation.
Common Mistakes Beginners Make
1. Trying to make Records mutable
The Mistake: Attempting to add setter methods or non-final fields to a record.
The Fix: Records are designed strictly for immutable data. If you need mutable state, use a standard class.
2. Deep Immutability Assumption
The Mistake: Assuming records are deeply immutable. If a record holds a reference to a mutable object (like a java.util.Date or an ArrayList), the contents of that object can still be modified.
The Fix: Use defensively copied collections (like List.copyOf()) or immutable objects as record components.
// Bad: The list can be modified outside the record
public record Team(String name, List<String> players) {}
// Good: Defensive copying in the compact constructor
public record SafeTeam(String name, List<String> players) {
public SafeTeam {
players = List.copyOf(players); // Makes the list unmodifiable
}
}
Summary
- Records provide a concise syntax for defining transparent, immutable data carriers.
- They automatically generate constructors, accessors,
equals,hashCode, andtoString. - They support compact constructors for easy validation and normalization.
- Records are implicitly final, shallowly immutable, and integrate perfectly with modern Java features like Pattern Matching.