records
data carriers in one line. what the compiler generates for you.
- chapter
- chapter 08.03
- reading time
- 6 min read
- java 16+
a record is a class that is just its data: record Point(int x, int y) {}. from that one line the compiler writes private final fields, a canonical constructor, accessors x() and y(), and equals, hashCode and toString based on the components. final in java 16.
records are shallowly immutable: the fields can't change, but an object a field points to (a list, say) still can.
the picture
you know the class with two fields, a constructor, two getters, equals, hashCode and toString, fifty lines that all say "this is a pair of ints"? and how adding a third field means editing six places, and forgetting one of them is a bug? a record says the data once and derives the rest.
think of a form with labelled boxes. a record is the form: the boxes are the components, and once it's filled in and stamped, nobody can rub a box out. two forms with the same answers are the same form, as far as anyone cares.
- 1public final class Point extends java.lang.Record {
- 2 private final int x;
- 3 private final int y;
- 4
- 5 public Point(int x, int y) {
- 6 this.x = x;
- 7 this.y = y;
- 8 }
- 9
- 10 public int x() { return x; }
- 11 public int y() { return y; }
- 12
- 13 public final boolean equals(Object o) { /* compare x, y */ }
- 14 public final int hashCode() { /* mix x, y */ }
- 15 public final String toString() { /* "Point[x=1, y=2]" */ }
- 16}
public final class Point extends java.lang.Record {private final int x;private final int y;public Point(int, int);public final java.lang.String toString();public final int hashCode();public final boolean equals(java.lang.Object);public int x();public int y();}
one private final field per component, and no others: a record can't declare extra instance fields. that's what makes it 'just its data'.
record Point(int x, int y) turns into, written out as java. the right side is javap -p on the real class file from javac 25. every member on the right came from that one line.using one
record Point(int x, int y) {}
void main() {
var p = new Point(1, 2);
IO.println(p); // toString
IO.println(p.x() + p.y()); // accessors
IO.println(p.equals(new Point(1, 2)) + " " + (p == new Point(1, 2)));
IO.println(p.hashCode() == new Point(1, 2).hashCode());
}Point[x=1, y=2] 3 true false true
equals compares values, == still compares identity: two different objects, equal contents. records can also have static fields, static methods, instance methods, nested types, and they can implement interfaces. what they can't have is extra state:
record R(int a) { int b; }
error: field declaration must be static
(consider replacing field with record component)compact constructors: validation without the boilerplate
the canonical constructor is generated, but you can write it yourself to check arguments. the compact form drops the parameter list and the field assignments: you write the checks, and the assignments happen after your code runs.
record Range(int lo, int hi) {
Range { // no (int lo, int hi), no this.lo = lo
if (lo > hi) throw new IllegalArgumentException(lo + " > " + hi);
}
}
void main() {
IO.println(new Range(1, 5));
IO.println(new Range(5, 1));
}Range[lo=1, hi=5] Exception in thread "main" java.lang.IllegalArgumentException: 5 > 1
inside a compact constructor, lo and hi are the parameters, and you can reassign them to normalize (hi = Math.max(hi, 0)). what you can't do is touch the fields: this.lo = lo there is error: cannot assign a value to final variable lo, because the compiler does that assignment itself, at the end.
shallowly immutable
the fields are final, so a record's components can never point somewhere else. but final is about the arrow, not the thing it points at:
members field is final: that arrow will always point at the same list. the list itself is an ordinary ArrayList, so anyone holding it can still add to it.record Team(String name, List<String> members) {}
var list = new ArrayList<>(List.of("ada"));
var t = new Team("core", list);
list.add("bob"); // t.members() now has bobrecord Team(String name, List<String> members) {
Team {
Objects.requireNonNull(name);
members = List.copyOf(members);
}
}List.copyOf makes an unmodifiable copy, so the record stops sharing the caller's list and t.members().add(...) throws UnsupportedOperationException. that's the record version of a defensive copy.
record Tagged(int[] data) {}
IO.println(new Tagged(new int[]{1}).equals(new Tagged(new int[]{1})));record Point(int x, int y) {}: fields, constructor, accessors, equals, hashCode, toString. one line.- a record is final, extends
Record, and has no state besides its components. - compact constructors validate and normalize. the field assignments happen after your code.
- equality is by value. arrays inside break that, because arrays compare by identity.
- shallowly immutable: copy mutable components with
List.copyOfand friends.