Records

Many classes simply carry data: a point, a money amount, a customer summary. Writing them by hand means a constructor, getters, equals, hashCode and toString — dozens of lines of boilerplate. A record (Java 16+) declares an immutable data class in one line.

Declaring a record

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

public class Main {
    public static void main(String[] args) {
        Point p = new Point(3, 4);
        Point q = new Point(3, 4);
        System.out.println(p);
        System.out.println(p.x() + ", " + p.y());
        System.out.println(p.equals(q));
        System.out.println(p == q);
    }
}
Output
Point[x=3, y=4]
3, 4
true
false

From the header record Point(int x, int y), Java generates:

  • private final fields x and y;
  • a constructor taking both values;
  • accessor methods x() and y() (no "get" prefix);
  • equals and hashCode based on all components, and a readable toString.

Records are immutable

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

public class Main {
    public static void main(String[] args) {
        Point p = new Point(1, 2);
        p.x = 10;
    }
}
Compiler error
Main.java:6: error: x has private access in Point

Record fields are private and final, and records have no setters, so there's no way to change a record after it's created. To "change" a record, create a new one: a common pattern is a method like withX(int newX) that returns new Point(newX, y).

Validation with a compact constructor

JAVA
record Money(long cents, String currency) {
    Money {
        if (cents < 0) throw new IllegalArgumentException("Negative amount");
        currency = currency.toUpperCase();
    }

    Money plus(Money other) {
        if (!currency.equals(other.currency)) throw new IllegalArgumentException("Currency mismatch");
        return new Money(cents + other.cents, currency);
    }

    @Override
    public String toString() {
        return "%d.%02d %s".formatted(cents / 100, cents % 100, currency);
    }
}

public class Main {
    public static void main(String[] args) {
        Money a = new Money(1999, "eur");
        Money b = new Money(501, "EUR");
        System.out.println(a.plus(b));
    }
}
Output
25.00 EUR

A compact constructor (no parameter list) runs before the fields are assigned: it can validate and normalise the arguments. Records can also have extra methods, static fields and methods, and can override toString.

Records as return values

JAVA
record Stats(int min, int max, double average) {}

public class Main {
    static Stats analyse(int[] values) {
        int min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
        long sum = 0;
        for (int v : values) {
            min = Math.min(min, v);
            max = Math.max(max, v);
            sum += v;
        }
        return new Stats(min, max, (double) sum / values.length);
    }

    public static void main(String[] args) {
        Stats s = analyse(new int[]{12, 7, 30, 18});
        System.out.println(s);
        System.out.println("Range: " + (s.max() - s.min()));
    }
}
Output
Stats[min=7, max=30, average=16.75]
Range: 23

Records are perfect for returning several values from a method, for map keys, and for data passed between layers of an application (DTOs).

Records with interfaces and static members

JAVA
record Celsius(double degrees) implements Comparable<Celsius> {
    static final Celsius FREEZING = new Celsius(0);

    static Celsius fromFahrenheit(double f) {
        return new Celsius((f - 32) * 5 / 9);
    }

    @Override
    public int compareTo(Celsius other) {
        return Double.compare(degrees, other.degrees);
    }
}

public class Main {
    public static void main(String[] args) {
        Celsius body = Celsius.fromFahrenheit(98.6);
        System.out.println(body);
        System.out.println(body.compareTo(Celsius.FREEZING) > 0);
    }
}
Output
Celsius[degrees=37.0]
true

Limitations

  • Records can't extend other classes (they implicitly extend java.lang.Record) and can't be extended.
  • They can't declare extra instance fields beyond their components.
  • Immutability is shallow: a record holding a List can't swap the list, but the list itself could still change. Copy collections in the compact constructor (items = List.copyOf(items);).

Try it yourself

Create an OrderLine record that calculates its own total, and sum several lines.

Try it yourself · Main.java
Edit the code, copy it into Main.java and run java Main.java (Java 25+). How to set up Java
Output
OrderLine[product=Ceramic Mug, quantity=2, unitPrice=12.5]
OrderLine[product=Desk Lamp, quantity=1, unitPrice=34.0]
Total: 59.0