Modern Java

32. Records — Custom Methods and Wither Pattern

Records support custom accessor overrides, additional constructors, and the "wither" pattern for creating modified copies — because records are immutable, you produce a new instance rather than mutating state.

Java 16+ (standard)

Explanation

You can override any generated accessor method to change its behaviour (e.g., return a defensive copy, normalise the value, or return an unmodifiable view). The overriding method must have the same name and return type as the component accessor.

Additional constructors must delegate to the canonical constructor (directly or indirectly) via this(...). This ensures all component fields go through any compact constructor validation.

The wither pattern (analogous to with methods in Kotlin data classes or Lombok) is a naming convention for methods that return a new record with one component changed: withX(int x) { return new Point(x, this.y()); }. Records are immutable — to "modify" them, you produce a new instance.

Example

import java.util.Collections;
import java.util.List;

public class RecordCustomMethods {
    record Name(String first, String last) {
        // Compact constructor — normalise
        Name {
            first = first.strip();
            last  = last.strip();
            if (first.isBlank() || last.isBlank())
                throw new IllegalArgumentException("Name parts cannot be blank");
        }

        // Custom accessor — combine components
        String full() { return first + " " + last; }

        // Wither pattern: produce modified copy
        Name withFirst(String newFirst) { return new Name(newFirst, last); }
        Name withLast(String newLast)   { return new Name(first, newLast); }
    }

    record ImmutableList<T>(List<T> items) {
        // Compact constructor: defensive copy + immutable view
        ImmutableList {
            items = Collections.unmodifiableList(List.copyOf(items));
        }

        // Override accessor to return unmodifiable (already done above, but explicit)
        @Override public List<T> items() { return items; }

        int size() { return items.size(); }
    }

    record Person(String name, int age) {
        // Alternate constructor: defaults age to 0
        Person(String name) { this(name, 0); }

        // Wither
        Person withAge(int newAge) { return new Person(name, newAge); }
    }

    public static void main(String[] args) {
        Name name = new Name("  Alice  ", "Smith");
        System.out.println(name.full());         // Alice Smith (stripped)
        System.out.println(name.withFirst("Bob")); // Name[first=Bob, last=Smith]

        Person p = new Person("Charlie");
        System.out.println(p);             // Person[name=Charlie, age=0]
        Person older = p.withAge(30);
        System.out.println(older);         // Person[name=Charlie, age=30]

        ImmutableList<String> list = new ImmutableList<>(List.of("a", "b", "c"));
        System.out.println("Size: " + list.size());
        try {
            list.items().add("d"); // UnsupportedOperationException
        } catch (UnsupportedOperationException e) {
            System.out.println("List is truly immutable");
        }
    }
}

Sources