Appearance
Lesson 05 · Records
Objectives
After this lesson you will be able to:
- Declare a record and explain what the compiler generates.
- Write canonical and compact constructors, and add extra constructors.
- Override generated members (accessors,
equals, …) and know when to. - Know the restrictions, and use generic, local, and nested records.
- Have a record implement interfaces and hold static members.
What a record is
A record is a transparent carrier for immutable data. From the component list the compiler generates: a canonical constructor, a private final field and an accessor per component (named x(), not getX()), plus equals, hashCode, and toString.
java
record Point(int x, int y) { }
Point p = new Point(1, 2);
p.x(); // 1 — accessor, NOT getX()
p.equals(new Point(1, 2)); // true — value-based equals
p.toString(); // "Point[x=1, y=2]"Exam trap
A record is implicitly final, its components are private final, and it cannot extends any class (it already extends java.lang.Record). It can implement interfaces. You cannot add extra instance fields — only the components are state (static fields are allowed).
Canonical and compact constructors
Every record has a canonical constructor matching the components. You can replace it with a compact form that omits the parameter list and runs before the implicit field assignments — ideal for validation or normalization. You assign to parameters, not this.x.
java
record Range(int lo, int hi) {
Range { // compact: no (int lo, int hi)
if (lo > hi) throw new IllegalArgumentException("lo > hi");
// fields lo, hi are assigned automatically AFTER this block
}
}You may instead write the explicit canonical constructor in full — then you must assign every field yourself — or add extra constructors that delegate via this(...):
java
record Temperature(double celsius) {
Temperature(double celsius) { // explicit canonical
this.celsius = Math.round(celsius * 10) / 10.0; // must assign the field
}
}
record Range2(int lo, int hi) {
Range2(int hi) { this(0, hi); } // extra constructor delegates to canonical
}Gotcha
In a compact constructor you assign normalized values to the parameter (lo = Math.min(...)), not this.lo — writing this.lo = ... there is a compile error. In an explicit canonical constructor it's the reverse: you must assign each this.field, or the record won't compile.
Overriding generated members
You may override any generated member — an accessor, equals, hashCode, or toString. The common reason is defensive copying of a mutable component, so the record stays effectively immutable.
java
record Team(String name, List<String> members) {
Team { // copy IN
members = List.copyOf(members);
}
@Override public List<String> members() { // copy OUT (here already unmodifiable)
return members;
}
}Gotcha
An overridden accessor should still return a value consistent with equals/hashCode. Returning something unrelated to the component breaks the record's value semantics.
Records implementing interfaces & static members
A record can implements interfaces and declare static fields, static methods (e.g. factories), and instance methods — just not extra instance fields.
java
record Money(long cents) implements Comparable<Money> {
public int compareTo(Money o) { return Long.compare(cents, o.cents); }
static Money ofDollars(long d) { return new Money(d * 100); } // static factory
}
Money.ofDollars(2).cents(); // 200Generic, local, and nested records
Records can be generic, declared locally inside a method (Java 16+), or nested (a nested record is implicitly static).
java
record Pair<A, B>(A first, B second) { } // generic
static List<Object> flatten(Object o) {
record Tagged(String tag, Object value) { } // local record
return List.of(new Tagged("x", o));
}Records as value types
Because equals/hashCode are component-based, records are perfect map keys and DTOs. They also work directly with record patterns (Lesson 08) for deconstruction.
SDET note
Records make excellent test fixtures and expected-value objects: value equality means assertEquals(expected, actual) "just works" without a hand-written equals. Prefer a record over a class with mutable getters/setters for test data — and copy mutable components defensively.
Key Takeaways
- A record auto-generates the canonical constructor, accessors (
x()), and value-basedequals/hashCode/toStringfrom its components. - It is implicitly
final, components areprivate final, noextends, no extra instance fields — but it can implement interfaces and have static members. - The compact constructor validates/normalizes by assigning to parameters; an explicit canonical constructor must assign every
this.field. Extra constructors delegate viathis(...). - You may override generated members (often to defensively copy a mutable component), keeping the result consistent with
equals. - Records can be generic, local, or nested (nested = implicitly
static); they're ideal for keys, DTOs, and record-pattern deconstruction.
Lesson Quiz
How do you read component x of record Point(int x, int y)?
Which is ILLEGAL for a record?
In a compact constructor, how do you normalize lo?
record Range(int lo, int hi) { Range { /* here */ } }An EXPLICIT canonical constructor must...
record Temperature(double celsius) { Temperature(double celsius) { /* ? */ } }Why might you override a record's accessor?
A nested record declared inside a class is...
What does new Point(1,2).equals(new Point(1,2)) return?
Can a record declare an extra instance field beyond its components?
Next: Sealed Types. Run the matching code in labs/src/main/java/com/jse21/m03_oop/.