Question 7 · Section 20

What is compact constructor in Record

It doesn't contain a signature — the compiler understands it's a compact constructor on its own.

Language versions: English Russian Ukrainian

🟢 Junior Level

Compact constructor is a special form of constructor in Record that is used only for validation and normalization of data.

It doesn’t contain a signature — the compiler understands it’s a compact constructor on its own.

public record User(String name, int age) {
    // Compact constructor — body only, no signature
    public User {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative");
        }
    }
}

Differences from regular constructor:

// Regular constructor
public User(String name, int age) {
    this.name = name;
    this.age = age;
}

// Compact constructor (validation only)
public User {
    if (age < 0) {
        throw new IllegalArgumentException();
    }
}

🟡 Middle Level

How it works

Compact constructor is inlined into the canonical constructor automatically:

public record Email(String value) {
    public Email {
        // This code will execute BEFORE field assignment
        if (value == null || !value.contains("@")) {
            throw new IllegalArgumentException("Invalid email");
        }
    }
}

// Compiler generates:
public Email(String value) {
    // Code from compact constructor
    if (value == null || !value.contains("@")) {
        throw new IllegalArgumentException("Invalid email");
    }
    // Then assignment
    this.value = value;
}

Important: Parameters in compact constructor are not final — you can change the value before assignment!

public record Email(String value) {
    public Email {
        value = value.toLowerCase().trim();  // ✅ normalization
    }
}

Common Mistakes

  1. Trying to assign this.value:
    public record User(String name) {
     public User {
         // ❌ Cannot use this.value
         this.name = name.toUpperCase();  // compilation error
    
         // ✅ Use plain name
         name = name.toUpperCase();  // OK — this is a parameter, not a field
     }
    }
    
  2. Trying to add fields:
    public record User() {
     public User {
         // ❌ Cannot add a field
         int extra = 0;  // compilation error
     }
    }
    

Practical Application

1. Validation:

public record Money(BigDecimal amount) {
    public Money {
        Objects.requireNonNull(amount, "Amount cannot be null");
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Negative amount");
        }
    }
}

2. Normalization:

public record PhoneNumber(String value) {
    public PhoneNumber {
        // Remove everything except digits
        value = value.replaceAll("[^0-9+]", "");
    }
}

3. Normalization with mutable objects:

public record TagCloud(List<String> tags) {
    public TagCloud {
        // Defensive copy
        tags = new ArrayList<>(tags);
        // Normalization
        tags.replaceAll(String::toLowerCase);
        tags.sort(null);
    }
}

🔴 Senior Level

Internal Implementation

Desugaring:

// Source code
public record User(String name, int age) {
    public User {
        if (age < 0) throw new IllegalArgumentException();
        name = name.toUpperCase();
    }
}

// Desugared code
public final class User extends Record {
    private final String name;
    private final int age;

    public User(String name, int age) {
        // Code from compact constructor
        if (age < 0) throw new IllegalArgumentException();
        name = name.toUpperCase();

        // Field assignment (generated by compiler)
        this.name = name;
        this.age = age;
    }
}

Restrictions:

  • Compact constructor parameters are effectively final, but not final
  • You can modify parameters before assignment
  • Cannot access this before assignment // Since the canonical constructor signature doesn’t contain throws, // checked exceptions must be wrapped in unchecked.
  • Cannot throw checked exceptions (because canonical constructor doesn’t declare throws)

Architectural Trade-offs

Compact constructor vs full constructor:

Aspect Compact Full
Validation ✅ Ideal ✅ Works
Normalization ✅ Works ✅ Works
Parameter modification ✅ Allowed ✅ Allowed
Delegation ❌ Cannot ✅ Can
Multiple constructors ❌ One ✅ Several

Edge Cases

1. Normalization of mutable objects:

public record DateRange(LocalDate start, LocalDate end) {
    public DateRange {
        Objects.requireNonNull(start);
        Objects.requireNonNull(end);
        if (start.isAfter(end)) {
            // Auto-correction
            var temp = start;
            start = end;
            end = temp;
        }
    }
}

2. Compact constructor with checked exception:

public record JsonData(String json) {
    public JsonData {
        try {
            Json.parse(json);
        } catch (JsonParseException e) {
            // ❌ Cannot throw checked exception
            // ✅ Wrap in unchecked
            throw new IllegalArgumentException("Invalid JSON", e);
        }
    }
}

3. Compact constructor + additional constructor:

public record Point(int x, int y) {
    // Compact — for validation
    public Point {
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException("Negative coordinates");
        }
    }

    // Additional — for convenience
    public Point(int value) {
        this(value, value);  // calls canonical (with validation)
    }
}

Performance

Compact constructor:
- Zero overhead — code is inlined into canonical constructor
- JIT can optimize validation
- No additional calls

Production Experience

Value objects with invariants:

public record Range(int min, int max) {
    public Range {
        if (min > max) {
            throw new IllegalArgumentException(
                "Min (%d) cannot be greater than max (%d)".formatted(min, max));
        }
    }

    public boolean contains(int value) {
        return value >= min && value <= max;
    }
}

// Usage
Range r = new Range(1, 10);  // OK
Range bad = new Range(10, 1);  // throws IllegalArgumentException

DDD Value Objects:

public record UserId(UUID value) {
    public UserId {
        Objects.requireNonNull(value, "UserId cannot be null");
    }

    public static UserId generate() {
        return new UserId(UUID.randomUUID());
    }

    public static UserId of(String value) {
        return new UserId(UUID.fromString(value));
    }
}

public record Email(String value) {
    private static final Pattern EMAIL_PATTERN =
        Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$");

    public Email {
        Objects.requireNonNull(value, "Email cannot be null");
        value = value.toLowerCase().trim();
        if (!EMAIL_PATTERN.matcher(value).matches()) {
            throw new IllegalArgumentException("Invalid email format");
        }
    }
}

Best Practices

// ✅ Use compact constructor for validation
public record Age(int value) {
    public Age {
        if (value < 0 || value > 150) {
            throw new IllegalArgumentException("Invalid age");
        }
    }
}

// ✅ For normalization of mutable objects
public record Tags(Set<String> tags) {
    public Tags {
        tags = Set.copyOf(tags);  // immutable copy
    }
}

// ❌ Don't use for complex logic
// ❌ Don't throw checked exceptions
// ❌ Don't access this before assignment

🎯 Interview Cheat Sheet

Must know:

  • Compact constructor — form without signature, only for validation and normalization
  • Compact constructor code is inlined into canonical BEFORE field assignment
  • Parameters can be modified: value = value.toLowerCase().trim() — this changes value before assignment
  • Cannot use this.field = ... — only parameter name
  • Checked exceptions cannot be thrown directly (canonical constructor doesn’t declare throws)
  • Cannot add fields in compact constructor

Common follow-up questions:

  • Why is compact constructor needed? — For validation and normalization without boilerplate
  • Can you change parameter value? — Yes, it’s normal practice: value = value.trim()
  • Why can’t you use this.field? — Field assignment is generated automatically after compact constructor
  • Can you throw checked exception? — No, must wrap in unchecked (IllegalArgumentException)

Red flags (DO NOT say):

  • ❌ “Compact constructor assigns fields” — Assignment is automatic, only parameters can be changed
  • ❌ “Compact constructor has a signature” — No signature, compiler determines by form
  • ❌ “You can create an extra field” — Instance fields are forbidden in Record
  • ❌ “You can throw IOException from compact constructor” — Only unchecked exceptions

Related topics:

  • [[1. What is Record in Java and since which version are they available]]
  • [[6. Can you override constructor in Record]]
  • [[8. Can you declare static fields and methods in Record]]
  • [[9. Are Record fields final]]