Compact constructors & validation in Java
Validating or normalizing record components.
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();
}
}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));
}Range[lo=9, hi=2]Range[lo=2, hi=9]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.
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());
}javaJAVACompile 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.
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
record Cart(List<String> items) { }The caller keeps a reference to the list and can change the Cart later.
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.
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
- No parentheses: Range { ... }
- Validate early and throw IllegalArgumentException
- Normalize by reassigning parameters: name = name.trim();
- Assigning this.field inside it is a compile error
💡 A security check at the door: every record must pass through it before it exists.
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());
}- ADA
- ada
- ada
- 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);
}- Range[lo=5, hi=1]
- Range[lo=1, hi=5]
- Throws IllegalArgumentException
- Compile error
Check your answer
Throws IllegalArgumentException. Validation in the compact constructor runs on every construction, so an invalid Range can never exist.