JavaRecords

Records

A record (introduced as a standard feature in Java 16) is a concise way to declare an immutable data-carrier class — a class whose entire purpose is to hold a fixed set of values. What used to take twenty or thirty lines of repetitive boilerplate can be expressed in a single line.

A record in one line

Java
record Point(int x, int y) {}

That single declaration automatically generates: a constructor that takes x and y, accessor methods, equals(), hashCode(), and a readable toString() — all without you writing a single line of their bodies.

Accessors: x() not getX()

Records deliberately break from the traditional JavaBean naming convention. Instead of getX(), the generated accessor for field x is simply x() — named exactly after the field, with no get prefix.

Using a record

Java
Point p = new Point(3, 4);
System.out.println(p.x());       // 3, not p.getX()
System.out.println(p.y());       // 4
System.out.println(p);           // Point[x=3, y=4] — generated toString()

Point p2 = new Point(3, 4);
System.out.println(p.equals(p2)); // true — generated equals() compares fields
System.out.println(p.hashCode() == p2.hashCode()); // true
Point[x=3, y=4]
true
true
Before vs. after: the boilerplate records eliminate

To achieve the exact same behavior — immutable fields, a constructor, accessors, a value-based equals()/hashCode(), and a readable toString() — as a traditional class, you'd need something like this:

The traditional, hand-written equivalent

Java
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 other = (Point) o;
        return x == other.x && y == other.y;
    }

    @Override
    public int hashCode() {
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point[x=" + x + ", y=" + y + "]";
    }
}
That's roughly 25 lines to express exactly the same idea as `record Point(int x, int y) `. Every method the record generates would need to be kept perfectly in sync by hand as fields are added or renamed — a common source of subtle bugs in older Java code.
Records are immutable by design

A record class is implicitly final — it cannot be subclassed — and every one of its fields is implicitly final too. There is no way to reassign x or y on an existing Point; if you need a “changed” point, you construct a brand-new one.

Compact constructors for validation

Sometimes you need to validate incoming values before they're stored. A compact constructor lets you do that without repeating the parameter list or the field assignments — the standard assignments still happen automatically after your validation code runs.

Validating in a compact constructor

Java
record Point(int x, int y) {
    Point { // compact constructor — no parameter list, no explicit assignments
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException("Coordinates must be non-negative: (" + x + ", " + y + ")");
        }
        // this.x = x; this.y = y; still happen automatically after this block
    }
}

public class Main {
    public static void main(String[] args) {
        Point valid = new Point(3, 4);     // fine
        Point invalid = new Point(-1, 4);  // throws IllegalArgumentException
    }
}
Warning
A compact constructor has no parentheses and no parameter list — just `Point { ... }`. Writing `Point(int x, int y) { ... }` instead defines a full **canonical constructor**, which requires you to assign every field yourself explicitly.
Note
Records pair extremely well with **sealed classes**, letting you model a fixed set of immutable data shapes and pattern-match over them exhaustively — see the dedicated **Sealed Classes** page.
  • record Point(int x, int y) {} generates a constructor, accessors, equals(), hashCode(), and toString().

  • Accessors are named after the field — x(), not getX().

  • Records are implicitly final, and every field is implicitly final — immutable by design.

  • A compact constructor validates or normalizes input without repeating field assignments.