Skip to content
Java modern java 8 min read

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(), not getName(). 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 memberDescription
private final int xOne field per component, always final
private final int ySame 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 be sealed (Java 17+)
  • Can be genericrecord 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/readObject customizations. 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:

  1. Extends java.lang.Record (a new abstract class added in Java 16).
  2. Has two private final fields.
  3. Implements equals, hashCode, and toString using special JVM instructions (invokedynamic backed by ObjectMethods in java.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.
  4. Has a canonical constructor marked with a special attribute so frameworks (like serialization and reflection) can identify it reliably via RecordComponent metadata 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

FeatureRecordRegular Class
BoilerplateMinimalHigh
FieldsImmutable, declared in headerMutable by default
InheritanceCannot extend; can implementFull inheritance
equals/hashCodeAuto-generated from all componentsManual or IDE-generated
toStringAuto-generated, readableManual
Suitable forData transfer, value objects, tuplesGeneral-purpose objects
Available sinceJava 16 (preview: Java 14)Always

Version History

Java VersionRecords Status
Java 14Preview (JEP 359)
Java 15Second preview (JEP 384)
Java 16Standard (JEP 395)
Java 21Record 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.

  • 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 instanceof and switch expressions.
  • 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.
Last updated June 13, 2026
Was this helpful?