Construction and destruction order
Trace the exact order fields, base constructors and subclass bodies run in, and stop trusting finalisers to release anything.
new Manager("Ada", 25) is not one step. The runtime allocates the object,
gives every field its default value, runs the base constructor to completion,
runs this class's field initialisers, and only then runs the constructor body
you wrote. Between the second step and the fourth, the object exists, has an
address, answers method calls — and is wrong.
Almost every construction bug is code that runs inside that window. Almost every cleanup bug is code that assumes the mirror window at the end is guaranteed.
What a constructor call runs, in order
Java fixes that order, and no field initialiser can jump ahead of it.
super(...) runs first, implicitly inserted if you did not write it, so a base
constructor always finishes before a single subclass field initialiser runs:
class Employee {
protected final String name;
Employee(String name) { this.name = name; }
}
class Manager extends Employee {
private final long created = System.nanoTime(); // 2. field initialisers
private final int rate;
Manager(String name, int rate) {
super(name); // 1. base constructor, to completion
this.rate = rate; // 3. constructor body, last
}
}
Python does not insert anything. __new__ allocates, __init__ initialises,
and the base __init__ is a plain method call you write yourself — so where
you put super().__init__() decides the order:
class Manager(Employee):
def __init__(self, name):
self.rate = 25 # before or after super() — your choice
super().__init__(name)
There is no zeroing step in Python: an attribute does not exist until it is
assigned, so a half-initialised object raises AttributeError rather than
handing back a quiet zero — louder, and it names the line.
The half-initialised subclass
The order above has one consequence people meet the hard way. A base constructor that calls an overridable method dispatches to the override — which belongs to a subclass whose fields have not been assigned yet.
class Employee {
Employee(String name) { System.out.println(describe()); }
String describe() { return "employee"; }
}
class Manager extends Employee {
private final int rate;
Manager(String name, int rate) { super(name); this.rate = rate; }
@Override String describe() { return "manager at " + rate + "/hr"; }
}
// new Manager("Ada", 25) prints: manager at 0/hr
rate is final and the constructor assigns it 25, and describe() still
reads 0, because super(name) finishes before that assignment runs. The blank
final is what makes the demonstration honest: write
private final int rate = 25; and the field is a constant variable
(JLS 4.12.4), so JLS 13.1 has javac fold the simple-name read at compile time —
it prints manager at 25/hr without touching a field. Box it
(private final Integer rate = 25;) and the folding stops: manager at null/hr.
Since Java 25, flexible constructor bodies (JEP 513) let a constructor run a
prologue before super(...) that may assign this class's own fields, so
{ this.rate = rate; super(name); } compiles and prints 25. It is a narrow
escape: the prologue may not read a field or call an instance method, and a
declaration-site initialiser still runs after super(...). The rule stands —
you cannot know what an override reads.
C++ changes the answer rather than the risk. During Employee's constructor the
object's dynamic type is Employee, so a virtual call resolves to
Employee::describe and never reaches the subclass — defined behaviour, and a
surprise in the opposite direction. Calling a pure virtual from a constructor
is undefined behaviour; most implementations abort with
pure virtual method called.
The rule that survives all three languages: a constructor may call only methods
that cannot be overridden — private, static or final in Java, non-virtual
in C++, name-mangled __private in Python. If a subclass has to supply a value,
it arrives as a constructor parameter, or a factory constructs first and calls
the hook second. Every object is then valid the moment its constructor returns,
which is the point of having one
(why one object owns a rule).
Destruction is the mirror, where it exists at all
C++ runs the reverse order and runs it deterministically: subclass destructor
body, then subclass members in reverse declaration order, then the base
destructor, triggered by the closing brace of a scope or by delete. The one
trap is deleting through a base pointer with a non-virtual destructor — that is
undefined behaviour, and in practice the subclass destructor is simply skipped.
A base class meant for inheritance declares virtual ~Base() = default;.
Java has no destructor at all. finalize() was deprecated in Java 9, deprecated
for removal in 18 (JEP 421, which also lets you switch finalisation off entirely
with --finalization=disabled), and is scheduled to be removed; Cleaner
replaced it with the same disclaimer — the action runs on another thread, at an
unspecified time, possibly never.
Python's __del__ looks deterministic and is not:
- It fires when the reference count reaches zero, and reference counting is a CPython implementation detail. On PyPy the same code releases later.
- In a reference cycle it waits for the cycle collector, which runs on allocation thresholds, not on your schedule. Since Python 3.4 those objects are collected, but the order finalisers run in is arbitrary.
- At interpreter shutdown module globals may already be
None, so a__del__callingos.closecan fail or be skipped. - An exception inside
__del__is printed and swallowed, so a failed flush propagates to nobody.
Release belongs to a scope, not a collector
The reliable version puts release in the caller's control flow, where an
exception cannot skip it: with in Python, and try-with-resources in Java,
which closes in reverse order of declaration and attaches a close failure as a
suppressed exception.
class Session:
def __enter__(self): return self
def __exit__(self, exc_type, exc, tb): self.sock.close() # runs on the throw path too
with Session(host) as s:
s.send(payload)
try (Session s = new Session(host)) {
s.send(payload);
} // close() runs on both paths, before the exception leaves
The cost of getting this wrong is not a heap problem, which is why it takes so
long to find. Take a service at 1 million requests/day: 1,000,000 ÷ 86,400 ≈ 12
QPS average, and peaks run 2–5× that, so call it 50 QPS. If each request opens a
socket that only a finaliser closes, and that generation is collected roughly
every 30 seconds, then 12 × 30 = 360 sockets sit open at average load and
50 × 30 = 1,500 at peak — past the common 1,024 open-file limit per process.
The process dies with Too many open files while the heap graph looks calm.
Deterministic release, or ownership a type enforces,
removes the arithmetic entirely; the estimate itself is ordinary
back-of-envelope work.
In an interview
What is being tested is whether you know the language runs code you did not write, in an order you can state. Say the order out loud: allocate, defaults, base constructor, field initialisers, constructor body — reverse on the way out where a destructor exists.
Useful phrasing: "I don't call overridable methods from a constructor. If a
subclass has to contribute a value, it comes in as a parameter, so the object is
valid the instant the constructor returns." And for cleanup: "The resource is
released by the scope — with or try-with-resources. A finaliser is at most a
leak detector, never the mechanism."
The mistake that loses points: answering "the finaliser closes it". It says you have never watched a service run out of file descriptors with a calm heap.
Check yourself
A Java base constructor calls an overridden method that reads a subclass field
declared private final int rate; and assigned this.rate = rate; after
super(name). What prints, and what would the same code print in C++?
Java prints 0 — nothing in the subclass runs until
super()returns. (Declaredprivate final int rate = 25;it prints 25, but only because of constant folding.) C++ prints the base class's version of the method: during base construction the dynamic type is still the base, so the virtual call never dispatches down.
A worker holds 200 sockets and each must be closed within a second of its
request finishing. Do you put the close in __del__? If not, what do you write
instead, and what does the cycle collector have to do with the answer?
No. Bind release to the scope that finished the request —
__exit__under awith, orclose()in afinally— the only version an exception cannot skip.__del__waits for the reference count to reach zero, and one cycle (a handler holding the socket, the socket's callback holding the handler) defers that to the cycle collector, which fires on allocation thresholds, not on a one-second deadline. At shutdown globals may already beNoneand the finaliser is skipped; an exception inside__del__is swallowed.
1 million requests/day, each holding one socket until a finaliser runs, with that generation collected every 30 seconds and peak at 4× average. How many sockets are open at peak, and what fails first?
1,000,000 ÷ 86,400 ≈ 12 QPS average, so peak ≈ 50 QPS; 50 × 30 = 1,500 sockets held. The file-descriptor limit (commonly 1,024) fails before memory does, so the symptom is
Too many open files, not an out-of-memory error.