Command Palette

Search for a command to run...

PHASE 4Beginner Java 16+ ~32 min· topic 9 of 12

Topic 4.9

Records

In one line

A record is a short way to declare a class that is just a carrier for immutable data. From one line, record Point(int x, int y) {}, Java generates private final fields, a constructor, accessor methods, and correct equals, hashCode and toString.

Think of it like this

A printed train ticket. It lists the passenger, the train, the seat and the date, and once printed it never changes. Two tickets with exactly the same details are interchangeable. You don't need any behaviour from a ticket; it carries data. A record is Java's built-in shape for exactly this kind of thing.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Record
A special kind of class, declared with record, whose job is to carry a fixed set of values. Java writes the boilerplate for you.
Component
One of the values listed in a record's header, like x and y in record Point(int x, int y).
Canonical constructor
The constructor whose parameters match the components exactly. Java generates it unless you write it.
Compact constructor
A canonical constructor written without a parameter list, used to check or adjust the values before the fields are set.
Accessor
A method that returns a component's value. In a record it has the component's name: p.x().
Boilerplate
Repetitive code you have to write but that carries no new idea, like hand-written getters, equals and hashCode.
Shallow immutability
The fields can't be reassigned, but objects they point to might still be changeable.
Record pattern
A Java 21 pattern like Point(int x, int y) that checks an object is a Point and pulls out its components in one step.

Step by step

01One line instead of fifty

Topic 4.8's Point needed two fields, a constructor, two getters, equals, hashCode and toString: around 40 lines, and every one a chance for a bug (forgetting a field in hashCode, say).

record Point(int x, int y) {} gives you all of it, correct by construction. javap -p shows exactly what the compiler generated.

terminal
$ javac Point.java
javap -p Point
── expected output ──
Compiled from "Point.java"
final class Point extends java.lang.Record {
private final int x;
private final int y;
Point(int, int);
public final java.lang.String toString();
public final int hashCode();
public final boolean equals(java.lang.Object);
public int x();
public int y();
}

02Using a record

Create it with new, like any class. Read components with accessor methods: p.x(). There are no setters; to "change" a record you create a new one (new Point(p.x() + 1, p.y())), often through a small with... method you write yourself.

Two records with equal components are equals and have equal hash codes, so they work as HashMap keys and in HashSets straight away.

Main.javawhole filejava
record Point(int x, int y) {
    Point withX(int newX) {          // records can have methods
        return new Point(newX, y);
    }
}

Point p = new Point(1, 2);
System.out.println(p);               // Point[x=1, y=2]
System.out.println(p.x());           // 1
Point q = p.withX(5);                // p is unchanged

03Validate with a compact constructor

A compact constructor has the record's name and no parentheses. Inside it, the component names refer to the parameters. You can check them, throw, or reassign them. After your code, the compiler adds this.lo = lo; this.hi = hi; automatically.

You can't assign the fields yourself in a compact constructor (this.lo = lo is an error there): that's what keeps the "exactly one assignment" rule simple.

Main.javawhole filejava
record Range(int lo, int hi) {
    Range {                                  // compact canonical constructor
        if (lo > hi) {
            throw new IllegalArgumentException("lo " + lo + " > hi " + hi);
        }
    }                                        // fields assigned here, automatically

    Range(int hi) {                          // any other constructor...
        this(0, hi);                         // ...must delegate to the canonical one
    }

    int length() { return hi - lo; }
}
Validate with a compact constructordiagram
Rendering diagram…

04What a record can and can't contain

Allowed in the body: instance methods, static fields, static methods, nested types, constructors that delegate, and overrides of the accessors, equals, hashCode or toString.

Not allowed: extra instance fields (field declaration must be static), instance initialiser blocks, extends (records already extend Record), or abstract. Records are always final, so class Big extends Range fails with cannot inherit from final Range.

A record declared inside another class is implicitly static, and since Java 16 you can declare a local record inside a method, which is perfect for an intermediate result in a calculation.

terminal
$ javac Main.java
── expected output ──
Main.java:1: error: field declaration must be static
record Point(int x, int y) { int z; }
^
(consider replacing field with record component)
1 error

05Shallow immutability: copy mutable components

record Order(String id, List<String> items) {} has a final field items, but the list itself can still be changed by whoever passed it in. The record isn't truly immutable.

Fix it in the compact constructor: items = List.copyOf(items); makes an unmodifiable copy (and rejects null elements). Now neither the caller nor anyone calling order.items() can change the record's contents.

Arrays are worse: the generated equals compares array components by reference, and you can't make an array unmodifiable. Prefer List components, or override equals/hashCode/toString with Arrays.equals/Arrays.hashCode/Arrays.toString.

06Record patterns: take a record apart (Java 21)

Because a record's state is exactly its components, Java can deconstruct it. if (obj instanceof Point(int x, int y)) checks the type and binds x and y in one step. Patterns nest: Line(Point(var x1, var y1), Point p2).

With switch pattern matching (Java 21) and sealed interfaces (Topic 5.11), records become Java's way of writing algebraic data types: each case of a shape, an expression or a message is a record, and the compiler checks you handled them all.

Main.javawhole filejava
static String describe(Object o) {
    return switch (o) {                         // Java 21
        case Point(int x, int y) when x == y -> "on the diagonal at " + x;
        case Point(int x, int y) -> "point " + x + "," + y;
        default -> "something else";
    };
}

Try it yourself

  1. 1

    Add a method and a static factory

    In the first example, add static Point origin() { return new Point(0, 0); } to the record and use it for the map lookup. Predict the output: does anything change?

  2. 2

    Break the defensive copy

    In the compact constructor example, delete items = List.copyOf(items);. Predict the Order line and the last line, then run. Two things change.

  3. 3

    Fix the array record

    In the array trap example, override equals and hashCode in Scores using Arrays.equals and Arrays.hashCode (cast the other object with o instanceof Scores s). Predict the first line again.

Code & diagrams

A record does the boilerplate for you Java 16+ New tab
Sign in to run this example in your browser.

Expected output

Point[x=3, y=4]
x = 3, y = 4
a == b: false, a.equals(b): true
same hash: true
distance: 5.0
a still Point[x=3, y=4], c is Point[x=10, y=4]
set size: 1
lookup: origin
Compact constructor: validate, normalise, copy Java 16+ New tab
Sign in to run this example in your browser.

Expected output

Range[lo=2, hi=5]
Range[lo=0, hi=7]
rejected: lo 5 > hi 2
Order[id=ORD-7, items=[tea, samosa]]
items() is unmodifiable
The array component trap Java 16+ New tab

The generated equals uses the array's own equals, which is identity. A List component compares by content.

Sign in to run this example in your browser.

Expected output

array records equal? false
arrays equal by content? true
toString starts with: Scores[values=[
list records equal? true
ScoreList[values=[1, 2, 3]]
Record patterns in instanceof and switch Java 21+ New tab
This example needs Java 21+. The in-browser compiler is Java 17: install JDK 21 or newer and run it with "java Main.java".

Expected output

point on the diagonal at 2
point 1,5
vertical line at x=3
line from Point[x=0, y=0] to Point[x=4, y=4]
unknown: hello
sum of components: 15

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Try to change a record's field

Inside the record, add void move() { x = 5; }.

terminal
$ javac Main.java
── what you'll see ──
Main.java:1: error: cannot assign a value to final variable x
record Point(int x, int y) { void move() { x = 5; } }
^
1 error

Break #2

Add an instance field to a record

Write record Point(int x, int y) { int z; }.

terminal
$ javac Main.java
── what you'll see ──
Main.java:1: error: field declaration must be static
record Point(int x, int y) { int z; }
^
(consider replacing field with record component)
1 error

Break #3

Run code before this(...) in a record constructor

Write a non-canonical constructor Range(int hi) { System.out.println("x"); this(0, hi); }.

terminal
$ javac Main.java
── what you'll see ──
Main.java:5: error: constructor is not canonical, so its first statement must invoke another constructor of class Range
Range(int hi) { System.out.println("x"); this(0, hi); }
^
Main.java:5: error: call to this must be first statement in constructor
Range(int hi) { System.out.println("x"); this(0, hi); }
^
2 errors

Myth vs fact

Myth

Records are fully immutable.

Fact

They're shallowly immutable: the fields are final, but a mutable component like an ArrayList or an array can still change. Copy such components in the compact constructor.

Myth

Records have getX() getters.

Fact

Accessors are named after the components: x(). Frameworks that only understand JavaBeans getX() conventions needed updates to support records (most major ones, like Jackson 2.12+, now do).

Myth

Records are just Lombok's @Data built in.

Fact

@Data makes mutable classes with setters. Records are final, immutable, transparent carriers whose shape the language understands, which is what enables record patterns and safer serialization.

Myth

You can't add methods or constructors to a record.

Fact

You can add instance and static methods, static fields, extra constructors (delegating to the canonical one) and implement interfaces. Only extra instance fields and inheritance are forbidden.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The generated equals, hashCode and toString aren't written out as bytecode loops: they're invokedynamic call sites bootstrapped by java.lang.runtime.ObjectMethods, which builds efficient method handles at first call. The hashCode algorithm is deliberately unspecified, so never persist or print record hash codes.

  • ▸

    Record serialization (Java 16) is special: deserialization always goes through the canonical constructor, so compact-constructor validation runs on untrusted input too. Ordinary classes are rebuilt without running constructors, which has been a long source of security bugs.

  • ▸

    Reflection exposes the shape: Point.class.isRecord() and getRecordComponents() return each component's name, type and accessor. Libraries like Jackson use this to map JSON to records without setters.

  • ▸

    Since records are final with final fields, the JIT can trust their fields more aggressively, and they're designed to migrate towards Project Valhalla's value classes, where identity-free objects may be flattened into arrays and other objects.

Remember this

  1. 1

    record Point(int x, int y) {} declares a record class with two components, x and y. The compiler generates: a private final field for each component; a canonical constructor Point(int x, int y) that assigns them; a public accessor for each, named after the component (x(), not getX()); and equals, hashCode and toString based on all components. toString looks like Point[x=1, y=2].

  2. 2

    Records arrived as a preview in Java 14, again in 15, and became a standard feature in Java 16 (JEP 395). A record is implicitly final (no subclasses), implicitly extends java.lang.Record, and can't extend anything else. It can implement interfaces, declare static fields and methods, add instance methods, and override the generated methods.

  3. 3

    A record can't declare extra instance fields: all state must be in the header. That's what makes the generated equals and hashCode complete. Writing int z; in the body gives field declaration must be static.

  4. 4

    To validate or normalise, write a compact constructor: Point { if (x < 0) throw ...; }, with no parameter list. It runs before the fields are assigned, and you can reassign the parameters (name = name.trim();); the compiler assigns the fields at the end. Any other constructor you add must delegate to the canonical one with this(...).

  5. 5

    Records are shallowly immutable. The fields are final, but if a component is a mutable object (a List, an array), its contents can still change. Copy it in the compact constructor (items = List.copyOf(items);). Arrays are a trap: the generated equals compares array components with ==-style reference equality, so two records holding equal arrays aren't equal.

  6. 6

    Records shine as value objects (money, coordinates, ranges), as HashMap keys (Topic 4.8's contract is generated for you), as return values carrying several results, and as messages and DTOs. With record patterns (Java 21) you can take them apart in instanceof and switch: if (o instanceof Point(int x, int y)).

Explain it without notes

01

What exactly does the compiler generate for record Point(int x, int y) {}?

02

What is a compact constructor, and what can and can't you do in it?

03

Why can't a record declare additional instance fields or extend another class?

04

Explain shallow immutability in records and the array-component pitfall.

05

When would you choose a record, and when a normal class?

Practice

01

Declare record Rgb(int r, int g, int b) that rejects values outside 0..255 and has a method hex() returning #RRGGBB in upper case. Print new Rgb(255, 165, 0).hex() and a rejected colour.

02

Use a local record inside main to find the longest word in {"tea", "samosa", "chai"} together with its length, and print the record.

03

Declare record Student(String name, int marks) and use a TreeMap<String, Student> keyed by name to store three students, then print the map.

Trade-offs

  • ↔

    Records remove boilerplate and bugs, but they fix your API to the component list: the accessors, constructor and equals are public promises, so you can't later hide or rename a component without breaking callers. Use a class when the representation should stay private.

  • ↔

    Records are immutable, so every "change" allocates a new object. That's cheap for small values and makes sharing between threads safe, but a hot loop that updates a big record millions of times may prefer a mutable class.

  • ↔

    Some frameworks (older JPA providers, JavaBeans-based tools) expect no-arg constructors and setters, so records don't fit everywhere. They're ideal for DTOs, API payloads, keys and messages.

Done when you can

  • Done when you can list everything a record generates, and the toString format.

  • Done when you can write a compact constructor that validates and normalises.

  • Done when you know what a record can't contain and why.

  • Done when you copy mutable components and avoid array components.

  • Done when you can deconstruct records with record patterns (Java 21).

  • Done when you can choose between a record and a class for a given type.