3 · 32 · 5 min
Bringing an object into existence
An object that exists in a half built state is a bug waiting to happen. The constructor's job is to make sure that never happens.
The invariant
Every class has rules that must always be true: a bank account's balance is never negative, a date's month is between 1 and 12, a connection either has a socket or is closed. Those are invariants, and the constructor is where they start being true.
public class Account {
private final String id;
private long balanceInPaise;
public Account(String id, long opening) {
if (id == null || id.isBlank())
throw new IllegalArgumentException("id is required");
if (opening < 0)
throw new IllegalArgumentException("opening balance cannot be negative");
this.id = id;
this.balanceInPaise = opening;
}
}Throwing from a constructor is correct. No object is produced, so no invalid object can be used. The alternative, constructing anyway and setting an isValid flag, puts the burden on every caller to check and one of them will not.
Order of construction
With inheritance, the base class is constructed first, all the way up the chain:
Animal() runs first
Dog() then this
Puppy() then thisThat order exists because a subclass may use fields the base class set up.
It also produces a trap worth knowing. Calling an overridable method from a constructor runs the subclass version, at a point where the subclass's own fields have not been initialised yet:
class Base {
Base() { setup(); } // calls the override
void setup() { }
}
class Derived extends Base {
private List<String> items = new ArrayList<>();
@Override void setup() { items.add("x"); } // items is still null
}The rule: do not call overridable methods from a constructor.
Too many parameters
A constructor taking eight arguments is unreadable at the call site and easy to get wrong when several share a type.
new Report(true, false, true, false, 30, 10, null, "x");The usual answers are a builder, which names each value, or a small parameter object grouping related ones. Either way the aim is that the call site says what it means.
Destruction
| Language | Cleanup |
|---|---|
| C++ | destructor, deterministic, at scope exit |
| Java, C# | garbage collected, timing unspecified |
| Python | reference counting, plus a cycle collector |
| Rust | ownership, dropped at scope exit |
In garbage collected languages, finalisers are not a reliable place to release a file handle or a socket, because nothing says when they run. That is what try-with-resources, using and with exist for: cleanup tied to scope rather than to collection.