Compact Constructors in Records
A record generates its constructor for you, but that constructor accepts anything: a range from 9 down to 2, a blank name, a list the caller can keep changing. A compact constructor lets you add checks and clean-ups without rewriting the assignments. You write only the extra logic; the compiler adds the field assignments at the end.
Validating and Normalizing
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class Compact {
record Range(int lo, int hi) {
Range {
if (lo > hi) {
throw new IllegalArgumentException("lo " + lo + " > hi " + hi);
}
}
Range(int single) { this(single, single); }
int length() { return hi - lo + 1; }
}
record Student(String name, List<String> subjects) {
Student {
name = Objects.requireNonNull(name).strip();
subjects = List.copyOf(subjects);
}
}
public static void main(String[] args) {
System.out.println(new Range(3, 7).length());
System.out.println(new Range(5));
try {
new Range(9, 2);
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
List<String> list = new ArrayList<>(List.of("Physics"));
Student s = new Student(" Advitha ", list);
list.add("Gaming");
System.out.println(s);
try {
s.subjects().add("Chemistry");
} catch (UnsupportedOperationException e) {
System.out.println("subjects are read-only");
}
}
}5
Range[lo=5, hi=5]
lo 9 > hi 2
Student[name=Advitha, subjects=[Physics]]
subjects are read-onlyHow it works
Range { ... }is a compact constructor: the record's name with no parameter list. The parametersloandhiare available inside it.- It validates: a reversed range throws, so an invalid
Rangecan never exist. - After the body, the compiler adds
this.lo = lo; this.hi = hi;automatically. - In
Student, the body reassigns the parameters:nameis stripped andsubjectsis replaced by an unmodifiable copy. The compiler then stores those new values. That's how a record becomes deeply immutable: the caller's laterlist.add("Gaming")doesn't reach the record. Range(int single)is an extra constructor. Every non-canonical constructor in a record must start by calling another withthis(...).
Compact vs Canonical
The canonical constructor is the one whose parameters match the components exactly. You can write it in full, or in the compact form:
| Compact form | Full canonical form |
|---|---|
Range { if (lo > hi) throw ...; } | Range(int lo, int hi) { if (lo > hi) throw ...; this.lo = lo; this.hi = hi; } |
| No parameter list | Parameter names and types must match the components |
| Field assignments added by the compiler | You assign every field yourself |
Writing this.lo = lo is an error | Required |
In a compact constructor, assigning a field directly fails:
R4.java:4: error: cannot assign a value to final variable lo
this.lo = lo;
^
1 errorCommon Mistakes
Writing this.name = ... in a compact constructor. Reassign the parameter instead: name = ....
Forgetting to copy mutable components. Without List.copyOf, the record stays only shallowly immutable.
Extra constructors that don't call this(...). Every one must delegate to the canonical constructor.
Practice
- Easy: Write
record Percentage(double value)with a compact constructor that rejects values below 0 or above 100. - Harder: Write
record Team(String name, List<String> members)that strips and capitalizes the name, rejects an empty member list, and stores an unmodifiable copy of the members.
Key Takeaways
Was this tutorial helpful?