skip to content

What is a compact canonical constructor in a Java record, and how does field assignment work in it?

level: middleimportance: must knowfreq 72%

answer

  1. No parameter list, no this.x = x;
  2. Implicit assignment happens at the end
  3. Normalize by reassigning the parameter
  4. this.field = ... is a compile error here
  5. Validation / normalization / defensive copy

basics

~20 s

A 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 s

A 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 lines
java
record 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 constructor

go deeper

for a junior

Recognizes the RecordName { ... } shape and that fields are assigned automatically.

for a middle

Explains the implicit end-of-block assignment, normalizes by reassigning the parameter, and knows this.field is illegal here.

for a senior

Uses the compact constructor for defensive copying and invariant enforcement, and contrasts it with the full canonical form's manual assignment.

for a principal

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.

context