Records
Java Records, standardized in Java 16, give you a special-purpose class designed for one job: holding immutable data. With a single line you get a constructor, accessor methods, equals(), hashCode(), and toString() — all generated automatically by the compiler.
The Problem Records Solve
Before records, writing a simple data-holding class meant a lot of ceremony. Imagine modeling a 2D point:
// The old way — lots of boilerplate for a very simple concept
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() { return x; }
public int y() { return y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return java.util.Objects.hash(x, y);
}
@Override
public String toString() {
return "Point[x=" + x + ", y=" + y + "]";
}
}
That is 25+ lines for two integers. Records collapse this to one:
record Point(int x, int y) {}
Output (calling new Point(3, 4).toString()):
Point[x=3, y=4]
Declaring a Record
The syntax is record ClassName(component list) { }. Each item in the component list is called a record component and declares both a private final field and a public accessor method of the same name.
record Person(String name, int age) {}
public class Main {
public static void main(String[] args) {
Person alice = new Person("Alice", 30);
System.out.println(alice.name()); // accessor, not getName()
System.out.println(alice.age());
System.out.println(alice); // toString() is automatic
}
}
Output:
Alice
30
Person[name=Alice, age=30]
Note: Record accessors are named after the component directly —
name(), notgetName(). This is intentional and follows the Java bean-style naming only if you add it yourself.
What the Compiler Generates
When you declare record Point(int x, int y) {}, the compiler automatically provides:
| Generated member | Description |
|---|---|
private final int x | One field per component, always final |
private final int y | Same for every component |
Point(int x, int y) | Canonical constructor — sets all fields |
int x() | Public accessor for each component |
int y() | Same for each component |
boolean equals(Object) | Field-by-field equality for all components |
int hashCode() | Derived from all component values |
String toString() | Readable ClassName[field=value, ...] format |
The Canonical Constructor
The canonical constructor is the one that accepts all record components in order. You can customize it without repeating the parameter list by using a compact constructor:
record Range(int min, int max) {
// Compact constructor — no parameter list, fields are assigned automatically
Range {
if (min > max) {
throw new IllegalArgumentException(
"min (%d) must be <= max (%d)".formatted(min, max)
);
}
}
}
public class Main {
public static void main(String[] args) {
Range r = new Range(1, 10);
System.out.println(r); // Range[min=1, max=10]
// Range bad = new Range(10, 1); // throws IllegalArgumentException
}
}
Output:
Range[min=1, max=10]
The compact constructor runs before the fields are assigned. You can validate or normalize parameters — but you cannot change which fields exist. The assignments happen implicitly at the end.
Tip: Use the compact constructor for validation and normalization. It keeps your record declaration clean while ensuring invalid states are impossible.
You can also write a full explicit canonical constructor if you prefer more control:
record Email(String address) {
Email(String address) {
this.address = address.toLowerCase().strip(); // explicit assignment required here
}
}
Adding Methods to Records
Records can have instance methods, static fields, and static methods. They cannot have instance fields beyond the record components.
record Circle(double radius) {
// A constant (static fields are allowed)
static final double PI = Math.PI;
// An instance method
double area() {
return PI * radius * radius;
}
double circumference() {
return 2 * PI * radius;
}
}
public class Main {
public static void main(String[] args) {
Circle c = new Circle(5.0);
System.out.printf("Area: %.2f%n", c.area());
System.out.printf("Circumference: %.2f%n", c.circumference());
}
}
Output:
Area: 78.54
Circumference: 31.42
Implementing Interfaces
Records can implement interfaces, which makes them very useful for sealed hierarchies (see Sealed Classes):
interface Shape {
double area();
}
record Circle(double radius) implements Shape {
public double area() {
return Math.PI * radius * radius;
}
}
record Rectangle(double width, double height) implements Shape {
public double area() {
return width * height;
}
}
public class Main {
public static void main(String[] args) {
Shape s = new Circle(3.0);
System.out.printf("%.2f%n", s.area()); // 28.27
}
}
Output:
28.27
Records and Pattern Matching
Records shine when combined with pattern matching. Java 21 introduced record patterns that let you destructure a record directly in a switch or instanceof expression:
record Point(int x, int y) {}
static String describe(Object obj) {
return switch (obj) {
case Point(int x, int y) when x == 0 && y == 0 -> "origin";
case Point(int x, int y) when x == 0 -> "on Y-axis";
case Point(int x, int y) when y == 0 -> "on X-axis";
case Point(int x, int y) -> "at (" + x + ", " + y + ")";
default -> "not a point";
};
}
public class Main {
public static void main(String[] args) {
System.out.println(describe(new Point(0, 0))); // origin
System.out.println(describe(new Point(0, 5))); // on Y-axis
System.out.println(describe(new Point(3, 4))); // at (3, 4)
}
}
Output:
origin
on Y-axis
at (3, 4)
Record patterns were previewed in Java 19–20 and became standard in Java 21.
Records as Local Records
You can declare records inside a method body — called a local record. This is handy when you need a temporary structured type for intermediate results:
import java.util.List;
public class Main {
public static void main(String[] args) {
record NamedScore(String name, int score) {}
var scores = List.of(
new NamedScore("Alice", 92),
new NamedScore("Bob", 87),
new NamedScore("Charlie", 95)
);
scores.stream()
.sorted((a, b) -> b.score() - a.score())
.forEach(ns -> System.out.println(ns.name() + ": " + ns.score()));
}
}
Output:
Charlie: 95
Alice: 92
Bob: 87
Restrictions on Records
Records are intentionally constrained so the compiler’s guarantees always hold:
- Cannot extend another class (they implicitly extend
java.lang.Record) - Cannot declare instance fields beyond the record components
- All component fields are
final— records are always immutable - Cannot be
abstract— but can besealed(Java 17+) - Can be
generic—record Pair<A, B>(A first, B second) {}is perfectly valid - Can implement any number of interfaces
Warning: Records cannot be subclassed. If you need an extensible hierarchy, combine them with sealed classes or use regular abstract classes.
Generic Records
Records fully support generics. This makes them excellent for building reusable container types:
record Pair<A, B>(A first, B second) {}
public class Main {
public static void main(String[] args) {
Pair<String, Integer> entry = new Pair<>("score", 100);
System.out.println(entry.first() + " = " + entry.second());
}
}
Output:
score = 100
Records and Serialization
Records implement Serializable when you declare it — just add implements Serializable. Because records are immutable and their canonical constructor is always used during deserialization, they avoid a classic serialization pitfall where the constructor is bypassed. This makes records a safer choice for serialized DTOs than ordinary classes.
import java.io.Serializable;
record Coordinate(double lat, double lon) implements Serializable {}
Note: Records do not support
writeObject/readObjectcustomizations. Use a custom serialization proxy pattern if you need advanced control.
Under the Hood
At the bytecode level, record Point(int x, int y) {} compiles to a class that:
- Extends
java.lang.Record(a new abstract class added in Java 16). - Has two
private finalfields. - Implements
equals,hashCode, andtoStringusing special JVM instructions (invokedynamicbacked byObjectMethodsinjava.lang.runtime) — meaning the implementations are generated at link time, not coded in the class file. This is faster than hand-coded reflection and easier for the JIT to inline. - Has a canonical constructor marked with a special attribute so frameworks (like serialization and reflection) can identify it reliably via
RecordComponentmetadata accessible through the Reflection API.
You can inspect a record’s components at runtime:
import java.lang.reflect.RecordComponent;
record Point(int x, int y) {}
public class Main {
public static void main(String[] args) {
for (RecordComponent rc : Point.class.getRecordComponents()) {
System.out.println(rc.getName() + " : " + rc.getType().getSimpleName());
}
}
}
Output:
x : int
y : int
This makes records very framework-friendly — Jackson, Hibernate, and Spring all have first-class record support starting from their respective modern versions.
Records vs Regular Classes — Quick Comparison
| Feature | Record | Regular Class |
|---|---|---|
| Boilerplate | Minimal | High |
| Fields | Immutable, declared in header | Mutable by default |
| Inheritance | Cannot extend; can implement | Full inheritance |
equals/hashCode | Auto-generated from all components | Manual or IDE-generated |
toString | Auto-generated, readable | Manual |
| Suitable for | Data transfer, value objects, tuples | General-purpose objects |
| Available since | Java 16 (preview: Java 14) | Always |
Version History
| Java Version | Records Status |
|---|---|
| Java 14 | Preview (JEP 359) |
| Java 15 | Second preview (JEP 384) |
| Java 16 | Standard (JEP 395) |
| Java 21 | Record patterns standard (JEP 440) |
Tip: If you are on Java 14 or 15 and want to try records, compile and run with
--enable-preview. For production, use Java 16 or later.
Related Topics
- Sealed Classes — Pair records with sealed classes to build exhaustive, type-safe domain models and unlock full pattern matching.
- Pattern Matching — Record patterns in Java 21 let you destructure record components directly inside
instanceofandswitchexpressions. - Switch Expressions — Switch expressions and record patterns work together to replace verbose if-else chains with clean, exhaustive dispatch.
- Immutable Class — See how to create immutable classes by hand — and appreciate how much records automate.
- Classes and Objects — Understand the foundation of Java classes before diving into the specialised record syntax.
- Java 21 LTS Features — Records get even more powerful in Java 21 with standard record patterns — explore everything else that landed in this release.