Records: Customization & Limits
Records: Customization & Limits
The previous two lessons introduced the record syntax and the automatically generated components. You know that record Point(int x, int y) gives you a constructor, accessors, equals, hashCode, and toString for free. This lesson goes one level deeper: what can you add to a record yourself, and where does the JVM draw a hard line?
Adding Instance Methods
A record is a fully fledged class. You can add as many instance methods as you like — they are read-only views or computations over the component values.
Notice that add does not modify this — it returns a brand-new instance. That is the idiomatic pattern for immutable value types.
Static Factory Methods
Constructors are inflexible: you call them with raw values and that is it. Static factory methods are a classic Java pattern that buys you named constructors, validation, caching, and descriptive intent.
Money.of("9.99", "USD") reads far better than new Money(999L, "USD").
Static Fields and Utility Constants
Records can have static fields. The prohibition on extra fields is limited to instance fields, because an extra instance field would break the guarantee that a record's value is fully described by its components.
Implementing Interfaces
Records can implement interfaces — this is a crucial extensibility hook. You often combine records with Comparable, custom marker interfaces, or sealed interface hierarchies.
What Records Cannot Do
The immutability guarantee comes with firm restrictions:
- No additional instance fields. Every piece of state must be declared as a component in the header. This is intentional — the record's identity is its components.
- Records cannot extend a class. They implicitly extend
java.lang.Recordand cannot extend anything else. This is because the JVM must be able to guarantee the component contract. - Records cannot be abstract. An abstract record would undermine the sealed, immutable value guarantee.
- Component accessors cannot be removed. You can override them (e.g. to add defensive copying for mutable component types), but you cannot hide or remove them.
- You cannot make a record mutable. Component fields are always
private final. Any attempt to reassign them inside the class is a compile error.
record Snapshot(List<String> items) — the reference is final but the list itself is not. Callers can still mutate snapshot.items().add("oops"). Fix it by making a defensive copy in the compact constructor: items = List.copyOf(items);
getX() accessor convention. Records use plain x() because they model data carriers, not mutable beans. Frameworks that require JavaBean-style getters (older Jackson, some JPA providers) may need explicit configuration or a custom serialiser.
Overriding the Generated Methods
All four generated methods — the canonical constructor, equals, hashCode, and toString — can be overridden:
Summary
Records are concise but not rigid. You can enrich them with instance methods, static factories, static constants, and interface implementations. The hard limits — no extra instance fields, no class inheritance, no mutability — are features, not bugs: they guarantee that a record's value is always fully and transparently described by the components you declared in its header. When you need more flexibility than records allow, a regular immutable class (with private finals and no setters) is the right tool.