🪆 Enums, Records & Nested Types · Intermediate

Compact constructors & validation in Java

Validating or normalizing record components.

🧩 The mysteryA record writes its constructor for you. But what if you need to reject bad data — or clean it up — before it's stored?

A constructor without parentheses

A compact constructor is written with just the record name: Range { ... } — no parameter list. Its body runs before the fields are assigned; then the compiler assigns the fields automatically at the end.

record Range(int lo, int hi) {
    Range {
        if (lo > hi)
            throw new IllegalArgumentException();
    }
}
🔮 Predict it

Invalid range

What happens?

record Range(int lo, int hi) {
    Range {
        if (lo > hi)
            throw new IllegalArgumentException();
    }
}
void main() {
    System.out.println(new Range(9, 2));
}
  1. Range[lo=9, hi=2]
  2. Range[lo=2, hi=9]
  3. Throws IllegalArgumentException
Show the answer

Validation runs on every construction, so an invalid Range can never exist. It isn't silently swapped either — it's rejected.

🔮 Predict it

Cleaning the input

The parameter is reassigned. What's stored?

record Tag(String name) {
    Tag {
        name = name.trim().toLowerCase();
    }
}
void main() {
    System.out.println(new Tag("  JAVA ").name());
}
  1. java
  2. JAVA
  3. Compile error: can't reassign a parameter
Show the answer

Reassigning the parameter normalizes it, and the compiler then copies that new value into the field. That's how you trim, lowercase or round inputs.

⚠️ The trap

Assigning the field yourself

You may reassign the parameter, but not the field. Writing this.years = years inside a compact constructor is a compile error — the compiler does that assignment for you.

record Age(int years) {
    Age {
        if (years < 0) years = 0;   // ok
        this.years = years;         // error
    }
}

Defensive copy

✗ Leaky
record Cart(List<String> items) { }

The caller keeps a reference to the list and can change the Cart later.

✓ Sealed tight
record Cart(List<String> items) {
    Cart { items = List.copyOf(items); }
}

List.copyOf stores an unmodifiable copy: neither the caller nor items() can change it.

The only door in

Every way of creating a record goes through its canonical constructor. Put validation there and no invalid object can exist. A static factory like Range.of(...) alone isn't enough — anyone could still call new Range(5, 1) directly.

💼 In the real world

Value objects

Teams wrap raw strings and numbers in validated records: Email, Money, Percentage. Once an Email exists, every method that receives one knows it's valid — no re-checking everywhere, fewer "how did this get into the database?" incidents.

Key takeaways

  1. No parentheses: Range { ... }
  2. Validate early and throw IllegalArgumentException
  3. Normalize by reassigning parameters: name = name.trim();
  4. Assigning this.field inside it is a compile error

💡 A security check at the door: every record must pass through it before it exists.

🤯 Did you know?

A record's equals, hashCode and toString aren't spelled out in bytecode. javac emits invokedynamic calls, and the JDK's ObjectMethods class generates the logic at runtime.

Practice questions

What does this print?

record Name(String value) {
    Name {
        value = value.trim().toUpperCase();
    }
}
void main() {
    System.out.println(new Name("  ada ").value());
}
  1. ADA
  2. ada
  3. ada
  4. Compile error: can't reassign a parameter
Check your answer

ADA. Reassigning the parameter normalizes it; the compiler then copies that new value into the field.

What does this print?

record Range(int lo, int hi) {
    Range {
        if (lo > hi)
            throw new IllegalArgumentException();
    }
}
void main() {
    var r = new Range(5, 1);
    System.out.println(r);
}
  1. Range[lo=5, hi=1]
  2. Range[lo=1, hi=5]
  3. Throws IllegalArgumentException
  4. Compile error
Check your answer

Throws IllegalArgumentException. Validation in the compact constructor runs on every construction, so an invalid Range can never exist.

Next: records are strict — no subclasses, no extra fields… and yet a record's contents can still change. How?