Skip to content

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();   // 200

Generic, 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-based equals/hashCode/toString from its components.
  • It is implicitly final, components are private final, no extends, 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 via this(...).
  • 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

Lesson Quiz · Records0 / 8
  1. How do you read component x of record Point(int x, int y)?

    • Ap.getX()
    • Bp.x
    • Cp.x()
    • Dp.get("x")
  2. Which is ILLEGAL for a record?

    • Aimplements Comparable
    • Bextends ArrayList
    • Ca static field
    • Da compact constructor
  3. In a compact constructor, how do you normalize lo?

    record Range(int lo, int hi) {
        Range { /* here */ }
    }
    • Athis.lo = Math.max(0, lo);
    • Blo = Math.max(0, lo);
    • CRange.lo = ...;
    • Dreturn Math.max(0, lo);
  4. An EXPLICIT canonical constructor must...

    record Temperature(double celsius) {
        Temperature(double celsius) { /* ? */ }
    }
    • Ado nothing — fields auto-assign
    • Bassign every field (this.celsius = ...)
    • Cassign the parameter (celsius = ...)
    • Dcall this()
  5. Why might you override a record's accessor?

    • ATo rename it to getX()
    • BTo return a defensive copy of a mutable component
    • CRecords can't have overridden accessors
    • DTo add a second component
  6. A nested record declared inside a class is...

    • Aimplicitly static
    • Bimplicitly abstract
    • Can inner (non-static) type
    • Dillegal
  7. What does new Point(1,2).equals(new Point(1,2)) return?

    • Afalse (different objects)
    • Btrue (value-based equals)
    • CCompile error
    • DNullPointerException
  8. Can a record declare an extra instance field beyond its components?

    • AYes
    • BNo
    • COnly if final
    • DOnly if static

Next: Sealed Types. Run the matching code in labs/src/main/java/com/jse21/m03_oop/.