📋 Section 20 · Question #6

Can You Override Constructor in Record

Any secondary constructor must delegate initialization through this(...) until it reaches the canonical constructor. Direct invocation of super(...) is forbidden in records.


🟢 Junior Level

Yes, you can. In Java, records support three constructor forms:

  1. Compact Constructor: A specialized constructor syntax without parameter parentheses, designed for validation and normalization:
    public record User(String name, int age) {
        public User {
            if (age < 0) {
                throw new IllegalArgumentException("Age cannot be negative: " + age);
            }
            name = name.trim(); // normalization
        }
    }
    
  2. Explicit Canonical Constructor: A full declaration matching all components in the record header:
    public record Point(int x, int y) {
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
    
  3. Secondary (Overloaded) Constructors: Constructors accepting alternative parameter sets that must explicitly delegate to the canonical constructor via this(...) on the first line:
    public record User(String name, int age) {
        public User(String name) {
            this(name, 18); // Mandatory this(...) invocation
        }
    }
    

Core Invariant

Any secondary constructor must delegate initialization through this(...) until it reaches the canonical constructor. Direct invocation of super(...) is forbidden in records.


🟡 Middle Level

1. Internal Mechanics of the Compact Constructor

The compact constructor is a syntax unique to Record. It omits the parameter list: public User { ... }.

What the compiler does under the hood:

  1. Implicitly passes parameters corresponding to the header components (name, age).
  2. Executes user validation and normalization logic.
  3. Automatically appends final field assignments at the very end of the method:
    this.name = name;
    this.age = age;
    
public record NormalizedEmail(String value) {
    public NormalizedEmail {
        Objects.requireNonNull(value, "Email must not be null");
        // 'value' here is the implicit method parameter, not the field!
        value = value.toLowerCase().trim();
        // The compiler automatically appends: this.value = value;
    }
}

[!WARNING] Inside a compact constructor, accessing fields via this.field is prohibited! Writing this.value = value; triggers a compilation error: “Cannot assign a value to final variable value”.

2. Defensive Copying in Constructors

If a record component is a mutable collection or array, initialization should create an unmodifiable defensive copy:

public record Team(String name, List<String> members) {
    public Team {
        Objects.requireNonNull(name);
        // Create an unmodifiable defensive copy:
        members = List.copyOf(members);
    }
}

🔴 Senior Level

Java Language Specification Rules (JLS §8.10.4)

The Java Language Specification enforces strict constraints on record constructors:

1. Mutually Exclusive: Compact vs. Explicit Canonical

A record cannot declare both a compact constructor and an explicit canonical constructor:

public record Point(int x, int y) {
    public Point { ... } // Compact constructor
    // ❌ Compilation error: duplicate method Point(int, int)
    public Point(int x, int y) { ... } 
}

The compact constructor is compiled directly into the canonical constructor bytecode. Declaring both creates duplicate methods with identical signatures.

2. Access Modifier Constraints

The access modifier of the canonical or compact constructor cannot be more restrictive than the record class itself:

public record User(String name) {
    // ❌ Compilation error: canonical constructor cannot have more restrictive access than the record class
    private User(String name) {
        this.name = name;
    }
}

If the class is public, the canonical constructor must be public.

However, secondary constructors can declare any access modifier, including private:

public record Token(String value) {
    // Canonical constructor is implicitly public
    
    // Secondary constructor can be private:
    private Token(byte[] raw) {
        this(Base64.getEncoder().encodeToString(raw));
    }

    public static Token fromBytes(byte[] raw) {
        return new Token(raw);
    }
}

3. Exception Throws Clause Restrictions

  • A compact constructor cannot declare a throws clause:
    public record FileData(String path) {
        // ❌ Compilation error: throws clause not allowed on compact constructor
        public FileData throws IOException { ... }
    }
    

    Validation failures must be thrown as RuntimeException subclasses (IllegalArgumentException, NullPointerException).

  • An explicit canonical constructor can declare throws CheckedException, but this is considered an anti-pattern because it disrupts functional instantiation and deserialization.

4 Tricky Questions

1. Can a Record declare both a compact constructor and an explicit canonical constructor simultaneously?

Answer: No, this causes a compilation error. A compact constructor is simply an alternative syntax for the canonical constructor. During bytecode generation, the compiler translates the compact constructor into a standard constructor with signature (ComponentType1, ComponentType2, ...). Declaring both creates two constructors with identical signatures, failing with a duplicate method compiler error.

2. Can the canonical constructor of a Record have a more restrictive access modifier than the Record class itself (e.g. private constructor on a public record)?

Answer: No, it cannot. Under JLS §8.10.4, the canonical constructor cannot be more restrictive than the record class. For a public record, the canonical constructor must be public. Records are transparent data carriers: any code with visibility to the record type must be able to construct it from its components. However, secondary (overloaded) constructors can have more restrictive access modifiers, including private.

3. Is it permitted to access fields via this.field inside a compact constructor (e.g. this.x = 10)?

Answer: No, accessing fields via this. inside a compact constructor is a compilation error. Inside a compact constructor, user code executes before the final fields are assigned. Component identifiers (x, y) in the compact constructor refer to implicit local parameters, not instance fields. You can reassign parameters (x = Math.max(0, x);), and the compiler appends this.x = x; at the end. Attempting to directly write or read this.x fails compilation.

4. Can a secondary constructor in a Record call super(...) instead of this(...)?

Answer: No, absolutely not. A secondary record constructor must invoke another constructor of the same record via this(...) on its very first line. Direct calls to super(...) are prohibited. The only superclass of any record is java.lang.Record, whose constructor is invoked exclusively and automatically by the canonical constructor.


🎯 Interview Cheat Sheet

Summary in 60 Seconds

  • Records support 3 constructor forms: Compact (public User { ... }), Explicit Canonical (public User(String name, int age) { ... }), and Secondary (public User(String name) { this(name, 0); }).
  • Compact Constructor: Omits parameter parentheses; used for validation and normalization; compiler generates this.field = param assignments automatically.
  • Secondary Constructors: Must delegate via this(...) on their first line; cannot call super(...).
  • Exclusivity: Cannot declare both a compact constructor and an explicit canonical constructor.
  • Accessibility: Canonical/compact constructor cannot be more restrictive than the record class.

Common Interview Traps

  • Trying to declare both compact and canonical constructors in the same record.
  • Writing this.field = value inside a compact constructor (forbidden).
  • Declaring a throws clause on a compact constructor.
  • Forgetting that secondary constructors must delegate via this(...).