What is Compact Constructor in Record
A Compact Constructor is a specialized syntactic form of the canonical constructor in a Record, created specifically for invariant validation and data normalization.
🟢 Junior Level
A Compact Constructor is a specialized syntactic form of the canonical constructor in a Record, created specifically for invariant validation and data normalization.
The primary syntactic feature of a compact constructor is the omission of parameter parentheses and the omission of manual field assignments (this.x = x):
public record User(String name, int age) {
// Compact constructor: only the record name and curly braces!
public User {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative: " + age);
}
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Name cannot be empty");
}
// Data normalization:
name = name.trim();
}
}
Comparison with Traditional Canonical Constructor
// Traditional canonical constructor (verbose boilerplate):
public User(String name, int age) {
if (age < 0) throw new IllegalArgumentException();
this.name = name.trim(); // Manual assignment to every field
this.age = age;
}
// Compact constructor (clean, focused on business invariants):
public User {
if (age < 0) throw new IllegalArgumentException();
name = name.trim();
// The compiler automatically synthesizes: this.name = name; this.age = age;
}
🟡 Middle Level
1. Compiler Desugaring Mechanics
The compact constructor is not a separate method in bytecode; it is syntactic sugar for the body of the canonical constructor.
During compilation, javac performs the following transformation:
- Generates method parameters matching the record components exactly:
(String name, int age). - Emits the developer’s validation and normalization statements at the beginning of the constructor.
- Automatically appends final field assignments at the very end of the method:
this.name = name; this.age = age;
[Constructor Arguments] -> [Compact Constructor Body] -> [Synthesized this.field = param] -> Object Instantiated
(Validation, Normalization)
2. Parameter Reassignment Before Storage
In a compact constructor, the component identifiers (name, age) refer to local parameters of the constructor, not fields of the instance. You can reassign them:
public record Money(BigDecimal amount, Currency currency) {
public Money {
Objects.requireNonNull(amount);
Objects.requireNonNull(currency);
// Round to 2 decimal places before saving to final field:
amount = amount.setScale(2, RoundingMode.HALF_UP);
}
}
3. Defensive Copying of Collections
If a record holds a collection or array, the compact constructor is the canonical location to create an unmodifiable defensive copy:
public record Order(String id, List<OrderItem> items) {
public Order {
Objects.requireNonNull(id);
// Defensive copy: List.copyOf creates an unmodifiable list
items = List.copyOf(items);
}
}
🔴 Senior Level
Java Language Specification Rules (JLS §8.10.4.2)
The Java Language Specification enforces strict structural constraints on compact constructor bodies:
1. Prohibited Access to this.field
Attempting to read or write instance fields via this inside a compact constructor causes a compilation error:
public record Point(int x, int y) {
public Point {
// ❌ Compilation error: cannot assign a value to final variable x
this.x = x;
// ❌ Compilation error: variable x might not have been initialized
System.out.println(this.x);
// ✅ Correct: manipulate the implicit local parameter
x = Math.max(0, x);
}
}
Rationale: Record fields are final, and the compiler enforces definite assignment exactly once in the synthesized epilogue of the constructor.
2. Throws Clause is Strictly Forbidden
Because a compact constructor does not declare a formal parameter signature, it cannot declare a throws clause:
public record Config(String path) {
// ❌ Compilation error: throws clause not allowed on compact constructor
public Config throws IOException { ... }
}
All validation and precondition failures must be raised as unchecked RuntimeException instances (IllegalArgumentException, NullPointerException, IllegalStateException).
3. Unbreakable Link to Java Deserialization
In standard Java classes, deserialization (ObjectInputStream.readObject) instantiates objects via native Unsafe allocation, bypassing all class constructors. This historically allowed malicious payloads to construct objects with corrupted states.
For Record types, the serialization specification mandates:
- Component values are read from the byte stream.
- The stream is required to invoke the record’s canonical constructor.
- The compact constructor code guaranteed executes during deserialization!
If an attacker manipulates the byte stream with invalid or negative values, the compact constructor throws IllegalArgumentException, aborting deserialization immediately.
4 Tricky Questions
1. What happens if you attempt to read or write a field via this.name inside a compact constructor?
Answer:
The compiler produces a compilation error.
Inside a compact constructor, direct access to the object’s instance state via this is blocked: instance fields have not yet been initialized, and their assignment is reserved for the compiler-synthesized epilogue. The identifier name refers to the implicit incoming constructor parameter, not the instance field. While you can reassign the parameter (name = name.toUpperCase()), accessing this.name is illegal.
2. Does the code inside a compact constructor execute during Java Object Serialization deserialization?
Answer:
Yes, it executes unconditionally.
This represents a major architectural distinction between records and ordinary classes. Normal classes bypass constructors during deserialization via native VM allocation, bypassing safety invariants.
For Record classes, the JVM specification requires ObjectInputStream to pass deserialized component values into the canonical constructor. Because compact constructors are compiled directly into the canonical constructor body, all validation rules, null checks, and defensive copies run during deserialization, preventing corrupted state injection.
3. Can you declare annotations on a compact constructor?
Answer:
Yes, you can.
You can apply any annotation whose target includes constructors (@Target(ElementType.CONSTRUCTOR)), such as @Deprecated, @JsonCreator, or custom validation markers. The compiler propagates these annotations directly to the synthesized canonical constructor in the class bytecode.
4. In what order do secondary constructors, compact constructors, and initializers execute?
Answer: When an instance is constructed via a secondary constructor:
- Expressions in the
this(arg1, arg2)invocation are evaluated. - Execution delegates to the canonical constructor.
- The compact constructor body executes first (running validations, defensive copies, and parameter reassignments).
- The compiler’s synthetic assignments execute (
this.comp = param). - The canonical constructor finishes, and execution returns to the secondary constructor body to execute any remaining statements.
🎯 Interview Cheat Sheet
Summary in 60 Seconds
- Syntax:
public User { ... }— No parentheses, no parameter list. - Primary Goal: Invariant validation (
if (x < 0) throw...) and data normalization (str = str.trim()). - Internal Mechanics: Code is placed in the canonical constructor before synthesized
this.field = param;assignments. - Rule of
this: Writingthis.field = ...inside a compact constructor is a compilation error; mutate local parameters instead. - Exceptions: Only unchecked exceptions (
RuntimeException). Athrowsclause is prohibited. - Security: The compact constructor executes during Java deserialization, preventing corrupted byte injections.
Common Interview Traps
- Attempting to write
this.x = xinside a compact constructor. - Believing a compact constructor can declare
throws CheckedException. - Using
new ArrayList<>(list)for defensive copying instead of unmodifiableList.copyOf(list). - Declaring both a compact constructor and an explicit canonical constructor in the same record.