Skip to content

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.

java
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:

code
Animal()    runs first
  Dog()     then this
    Puppy() then this

That 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:

java
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.

java
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

LanguageCleanup
C++destructor, deterministic, at scope exit
Java, C#garbage collected, timing unspecified
Pythonreference counting, plus a cycle collector
Rustownership, 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.