What is a compact canonical constructor in a Java record, and how does field assignment work in it?
answer
- No parameter list, no this.x = x;
- Implicit assignment happens at the end
- Normalize by reassigning the parameter
- this.field = ... is a compile error here
- Validation / normalization / defensive copy
basics
~20 sA compact constructor uses the record's name with no parameter list and no field assignments. You write only validation or cleanup code; the compiler automatically assigns all the components to their fields at the very end.
solid answer
~50 sA compact canonical constructor is a shorthand for the canonical constructor that omits the parameter list entirely — you write `Range { ... }` instead of `Range(int lo, int hi) { ... }`. Inside, the component names are in scope as the incoming parameters, so you use them for validation and normalization. Crucially, you do NOT write `this.lo = lo;` — the compiler implicitly assigns every component to its field after your code block runs. This means any reassignment you do to a parameter name (e.g. `name = name.trim();`) is what actually gets stored, because the implicit assignment happens at the end using the current parameter values. The compact form is the idiomatic place for argument checks, trimming/normalizing strings, clamping ranges, and defensive copies. Because the assignment is implicit and automatic, you can never accidentally forget a field, which is a common bug in a full canonical constructor.
code
java · 12 linesrecord User(String name, List<String> roles) {
User { // compact canonical constructor
if (name == null || name.isBlank())
throw new IllegalArgumentException("name required");
name = name.trim(); // normalize: reassign the parameter
roles = List.copyOf(roles); // defensive copy (also null-checks)
// compiler then runs implicitly:
// this.name = name; this.roles = roles;
}
}
// this.name = name; // <-- ILLEGAL inside a compact constructorgo deeper
Recognizes the RecordName { ... } shape and that fields are assigned automatically.
Explains the implicit end-of-block assignment, normalizes by reassigning the parameter, and knows this.field is illegal here.
Uses the compact constructor for defensive copying and invariant enforcement, and contrasts it with the full canonical form's manual assignment.
Treats the compact constructor as the canonical invariant gate for value objects and reasons about how it interacts with serialization and equals/hashCode derivation.
## Background: the canonical constructor Every **record** has a **canonical constructor** — the constructor whose parameters match the record's **components** (the named data fields declared in the record header). You can let the compiler generate it, or write it yourself when you need validation or cleanup. ## The two ways to write it yourself ### 1. Full canonical constructor You write the complete signature and assign every field manually: ```java record Range(int lo, int hi) { Range(int lo, int hi) { // full form: parameters spelled out if (lo > hi) throw new IllegalArgumentException("lo > hi"); this.lo = lo; // you MUST assign every field yourself this.hi = hi; } } ``` If you forget `this.hi = hi;`, the code does not compile (definite-assignment error on a final field). ### 2. Compact canonical constructor The **compact** form drops the parameter list and all the explicit field assignments: ```java record Range(int lo, int hi) { Range { // no (int lo, int hi), no body assignments if (lo > hi) throw new IllegalArgumentException("lo > hi"); } } ``` Notice three things: 1. There is **no parameter list** — just the record name followed by `{`. 2. The component names (`lo`, `hi`) are **in scope** as the constructor's implicit parameters, so you can read and even reassign them. 3. There are **no `this.lo = lo;` lines**. The compiler **implicitly assigns every component to its field at the very end** of the block, *after* your code runs. ## Why the implicit-assignment timing matters Because assignment happens at the end using the **current** values of the parameter variables, **normalization works by reassigning the parameter**: ```java record User(String name) { User { name = name == null ? "" : name.trim(); // reassign the PARAMETER // implicit: this.name = name; <-- uses the trimmed value } } ``` The stored field ends up with the trimmed value, even though you never wrote `this.name = ...`. A frequent misconception is writing `this.name = name.trim();` inside a compact constructor — that is a **compile error**, because you may not assign the fields explicitly in the compact form. ## What it's used for - **Validation:** throw `IllegalArgumentException`/`NullPointerException` on bad input. - **Normalization:** trim strings, lowercase, clamp numbers — by reassigning the parameter. - **Defensive copying:** wrap a mutable argument, e.g. `tags = List.copyOf(tags);`, so the record stays immutable. ## Benefits over the full form You cannot accidentally forget to assign a field (the compiler does it), and the constructor reads as pure intent — only the checks and transformations you care about, with the boilerplate removed.
- Why can normalization be done by reassigning the parameter inside a compact constructor?Because the compiler performs the implicit field assignment at the END of the block using the current parameter values. If you reassign `name = name.trim();`, that trimmed value is what gets stored.
- What happens if you write `this.lo = lo;` inside a compact constructor?It is a compile error. The compact form forbids explicit field assignment because the compiler supplies all the assignments implicitly at the end.
The compact constructor is like a quality-control checkpoint on a conveyor belt: each item passes by labeled with its value, you inspect or relabel it, and the machine at the end of the belt automatically boxes whatever value the label now shows — you never touch the box yourself.
saying these in an interview costs you the question
- Writing this.field = field inside a compact constructor (compile error).
- Believing the implicit assignment happens before your code, so reassigning the parameter would have no effect — it actually happens after.
- Thinking a compact constructor needs a parameter list — it must omit it.