Explain the canonical constructor and the compact constructor in records, including the rules for overriding accessors.
answer
- Compact ctor: no param list, no field assignments
- Compiler appends this.x = x after compact body
- Explicit canonical: you assign ALL fields
- Reassign params in compact to normalize
- Accessor override must be public + return component type
basics
~20 sEvery record has a canonical constructor that takes all components. You can write it explicitly, or use a compact constructor (no parameter list) to validate or normalize inputs - the fields are assigned for you afterward. You can also override accessors, but they must return the right type.
solid answer
~50 sThe canonical constructor is the one whose parameters match the record components; if you don't write it, the compiler generates one that assigns each parameter to its field. You can replace it with an explicit canonical constructor, but then you must assign every field yourself. More idiomatic is the compact constructor: you omit the parameter list entirely, validate or normalize the implicit parameters, and the compiler appends `this.x = x` for each component automatically - so you must NOT assign fields yourself there. Use it for invariant checks (throwing on bad input) or normalization (trimming strings, copying mutable collections). For accessors, you may override any generated one - for instance to return a defensive copy - but the override must be public and return the component's type. Overrides must not weaken the contract: equals/hashCode must stay consistent if you touch them.
code
java · 19 linesrecord Name(String first, String last) {
// compact constructor: validate + normalize
Name {
if (first == null || last == null)
throw new NullPointerException();
first = first.strip(); // reassigned param -> stored in field
last = last.strip();
// compiler appends: this.first = first; this.last = last;
}
}
record Box(java.util.List<String> items) {
Box {
items = java.util.List.copyOf(items); // defensive copy in
}
public java.util.List<String> items() { // defensive copy out
return java.util.List.copyOf(items);
}
}go deeper
Knows a compact constructor exists for validation and that you throw to reject bad input.
Distinguishes compact vs explicit canonical, knows the compiler auto-assigns fields in the compact form, and can normalize by reassigning parameters.
Uses compact-ctor copying plus accessor overrides to make records with mutable components truly immutable, and keeps equals/hashCode consistent.
Sets team conventions on validation placement, defensive-copy strategy, and how constructor invariants interact with deserialization (which can bypass them) at scale.
### The canonical constructor Every record has a **canonical constructor**: the constructor whose signature exactly matches the record's components, in order. For `record Range(int lo, int hi)` it is `Range(int lo, int hi)`. If you write nothing, the compiler generates it and it simply does `this.lo = lo; this.hi = hi;`. You can provide it **explicitly**: ```java record Range(int lo, int hi) { Range(int lo, int hi) { // explicit canonical constructor if (lo > hi) throw new IllegalArgumentException("lo > hi"); this.lo = lo; // you MUST assign every field yourself this.hi = hi; } } ``` The rule: in an explicit canonical constructor, **you are responsible for assigning all the fields.** ### The compact constructor The **compact constructor** is syntactic sugar for the common case of validate/normalize. You write the record name with **no parameter list and no parentheses**: ```java record Range(int lo, int hi) { Range { // compact constructor if (lo > hi) throw new IllegalArgumentException("lo > hi"); // NO field assignments here } } ``` Behind the scenes the parameters (`lo`, `hi`) are implicitly in scope. After your code runs, the compiler **automatically appends `this.lo = lo; this.hi = hi;`**. Therefore: - You **must not** write `this.lo = lo` yourself - the assignment is added for you. - You **may reassign the parameters** to normalize them; the (possibly changed) parameter values are what get assigned to the fields: ```java record Name(String first, String last) { Name { first = first.strip(); // normalization - this strip()ed value is stored last = last.strip(); } } ``` - You **may not** declare a return type, and you may throw to reject invalid input. You choose **either** an explicit canonical **or** a compact constructor, never both for the same record. ### Overriding accessors Each component gets a generated accessor named exactly like the component (`lo()`, not `getLo()`). You can override it: ```java record Inventory(List<String> items) { public List<String> items() { // override return List.copyOf(items); // defensive copy } } ``` Rules for an accessor override: - It must be **`public`**. - It must **return the component's type** (covariant returns are allowed). - It must not throw checked exceptions not in the implicit signature. This is the standard place to defend against exposing a mutable component (e.g. returning an unmodifiable copy of a list). Combine it with copying in the compact constructor for full immutability. ### Why both forms exist The compact form removes boilerplate and prevents a classic bug: forgetting to assign a field. The explicit form is an escape hatch when you genuinely need full control (rare). Validation and normalization in the constructor are the chief reasons to customize a record at all - they let the record *enforce its invariants* while staying a transparent data carrier.
- In a compact constructor, how do you normalize a value before it is stored?Reassign the implicit parameter, e.g. `name = name.strip();`. The compiler stores the parameter's final value into the field, so the normalized value is what gets persisted.
- Why might you override an accessor to return a copy?If a component is a mutable type (like a List or array), the generated accessor returns the same reference, letting callers mutate the record's internals. Overriding it to return an unmodifiable or copied view preserves immutability.
saying these in an interview costs you the question
- Writing this.x = x inside a compact constructor (causes confusion / is unnecessary)
- Thinking you can have both a compact and an explicit canonical constructor
- Giving an accessor override a different return type or non-public access
- Believing the compact constructor changes the field directly - it changes the parameter, then the field is assigned