Records

Java · Practical · 10 min

Most applications pass lots of plain data around, and records remove the boilerplate while making it immutable by default.

A record (Java 16+) is a class whose purpose is to carry data. One line declares the components, and the compiler generates the rest:

record Point(int x, int y) { }

You get: private final fields, a canonical constructor taking all components, accessor methods named after the components (p.x(), not getX()), and equals, hashCode and toString based on all components. Two records with the same values are equal, which makes them ideal map keys and test expectations.

Records are shallowly immutable and implicitly final; they can't extend another class (they implicitly extend java.lang.Record), but they can implement interfaces.

You can add validation with a compact constructor, which omits the parameter list; the field assignments happen automatically at the end:

record Range(int lo, int hi) {
    Range {
        if (lo > hi) throw new IllegalArgumentException(lo + " > " + hi);
    }
}

Records can also have static fields and methods, extra instance methods, and additional constructors that delegate to the canonical one. They cannot declare additional instance fields.

"Shallowly" immutable matters: if a component is a mutable List, callers can still modify that list. Copy it in the compact constructor with items = List.copyOf(items);.

Use records for DTOs, API responses, value objects, multiple return values and keys. Use a regular class when you need mutable state, inheritance, or to hide the representation.

Example

import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class Records {
    record Point(int x, int y) {
        Point {                                   // compact constructor
            if (x < 0 || y < 0) throw new IllegalArgumentException("negative");
        }
        double distanceTo(Point o) { return Math.hypot(x - o.x, y - o.y); }
    }
    record Order(String id, List<String> items) {
        Order { items = List.copyOf(items); }     // defensive copy
    }

    public static void main(String[] args) {
        var a = new Point(3, 4);
        System.out.println(a);                    // Point[x=3, y=4]
        System.out.println(a.x() + " " + a.distanceTo(new Point(0, 0))); // 3 5.0

        Set<Point> seen = new HashSet<>();
        seen.add(new Point(1, 2));
        System.out.println(seen.contains(new Point(1, 2))); // true: value equality
    }
}

Remember

  • record R(A a, B b) generates constructor, accessors, equals/hashCode/toString
  • Validate or normalise in a compact constructor
  • Records are shallowly immutable — copy mutable components

Watch out for

  • Calling getX() on a record; accessors are named exactly like the components, e.g. x().

Check yourself

  1. Given `record P(int x) {}`, what does `new P(1).equals(new P(1))` return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.oracle.com/en/java/javase/21/language/records.html