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
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); } }
Point[x=3, y=4] 3, 4 true false
From the header record Point(int x, int y), Java generates:
- private
finalfieldsxandy; - a constructor taking both values;
- accessor methods
x()andy()(no "get" prefix); equalsandhashCodebased on all components, and a readabletoString.
Records are immutable
record Point(int x, int y) {} public class Main { public static void main(String[] args) { Point p = new Point(1, 2); p.x = 10; } }
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
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)); } }
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
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())); } }
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
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); } }
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
Listcan'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.
OrderLine[product=Ceramic Mug, quantity=2, unitPrice=12.5] OrderLine[product=Desk Lamp, quantity=1, unitPrice=34.0] Total: 59.0