Modern Java

31. Records

Records (Java 16+) are transparent, immutable data carriers that automatically generate a canonical constructor, component accessors, equals(), hashCode(), and toString() from the record header.

Java 16+ (standard)

Explanation

A record declaration record Point(int x, int y) {} generates: a private final int x; private final int y; field for each component, a canonical constructor Point(int x, int y) that assigns all fields, accessor methods x() and y() (not getX()/getY()), and correct equals(), hashCode(), and toString().

Records are implicitly final (cannot be extended), cannot extend other classes (they implicitly extend java.lang.Record), and all fields are private final. They can implement interfaces, declare static fields and methods, declare instance methods, and have multiple constructors.

The compact constructor lets you validate or normalise arguments without repeating assignments โ€” parameter assignments happen automatically after the compact constructor body: record Range(int lo, int hi) { Range { if (lo > hi) throw new IllegalArgumentException(); } }.

Example

import java.util.List;

public class RecordsDemo {
    // Basic record
    record Point(int x, int y) {
        // Compact constructor: validate, then assignments happen automatically
        Point {
            if (Double.isNaN(x) || Double.isNaN(y)) throw new IllegalArgumentException();
        }

        // Instance method
        double distanceTo(Point other) {
            int dx = this.x - other.x, dy = this.y - other.y;
            return Math.sqrt(dx * dx + dy * dy);
        }

        // Static factory
        static Point origin() { return new Point(0, 0); }
    }

    // Record implementing an interface
    interface Shape { double area(); }
    record Circle(double radius) implements Shape {
        // Compact constructor for validation
        Circle { if (radius <= 0) throw new IllegalArgumentException("radius must be positive"); }
        @Override public double area() { return Math.PI * radius * radius; }
    }

    // Generic record
    record Pair<A, B>(A first, B second) {}

    public static void main(String[] args) {
        Point p1 = new Point(1, 2);
        Point p2 = new Point(3, 4);

        System.out.println(p1);           // Point[x=1, y=2]
        System.out.println(p1.x());       // 1  (accessor, not getX())
        System.out.println(p1.equals(new Point(1, 2))); // true
        System.out.printf("Distance: %.2f%n", p1.distanceTo(p2));

        Circle c = new Circle(5);
        System.out.printf("Circle area: %.2f%n", c.area());

        // Validation
        try { new Circle(-1); }
        catch (IllegalArgumentException e) { System.out.println("Caught: " + e.getMessage()); }

        Pair<String, Integer> pair = new Pair<>("Alice", 30);
        System.out.println(pair); // Pair[first=Alice, second=30]

        // Records work great with streams and collections
        List<Point> points = List.of(new Point(0, 0), new Point(3, 4), new Point(1, 1));
        points.stream()
              .filter(p -> p.x() > 0)
              .forEach(System.out::println);
    }
}

Sources