Lesson 01 / 13
Class, Object, and Constructor
The constructor chain runs top to bottom, and an overridden method called from the chain's upper link comes from the runtime type: since the subclass's fields are not yet assigned, the result comes out `sub: null x0`; in a fragile version the object is never delivered — the trap does not arise in a private or final method.
Contents
The Java Fundamentals course used a class only as a container: what was measured was what the source left in the class file, the class itself never entered the measurement. Even at the closing course, reading assignment’s single model, a class was still a frame — a field, a method, nothing more.
In this course, the container becomes the subject. The moment it does, something new appears in the source: a name starts carrying two types. When a variable is declared, the compiler sees one written type; the actual class of the object assigned to it can be something else entirely. Every lesson here asks a single question: which one made this decision? The first lesson asks it at the moment an object is constructed, because during construction the two types do not agree with each other for a while.
One Name, Two Types
A declaration carries two separate pieces of information. In Base d = new Sub();, Base is
the declared type: the type the compiler looks at while resolving the name, deciding which
member can be written. Sub is the runtime type: the object’s real class, unchanged until
garbage collected. In most programs the two are written the same, and the difference does not
show.
The course deliberately separates them. The core is a base class (Base) and a subclass
(Sub) extending it. Sub overrides one of Base’s methods — puts its own version under
the same signature — and also declares a field with the same name. A single reference is set
up: declared type Base, runtime type Sub. The oracle is the rig itself; since we separated
the two types ourselves, which one an answer comes from is known from the start.
This lesson’s question is: while the object is still under construction, what state are the two types in? A constructor is not a method, but its body can contain a method call. When that call runs, which object is there?
Object-oriented programming’s concepts — object, class, inheritance, interface — were established in the Programming Fundamentals course and are not repeated here. What is measured is not the concept, it is which side the decision comes from.
The Measurement Core
The measurement is a setup trace: every initialization step writes its name and the result it sees at that moment to a shared list, and the list is printed once at the end.
- CI1 — Declared type
Base, runtime typeSub, is set up by the rig itself; which type every line comes from is read from the rig, not from inference. - CI2 — The trace is written to a shared list and printed at the program’s end; printing does not mix into the measured order.
- CI3 — The
Subclass’scountfield has no initial value and is assigned only in the constructor body; since its unassigned state shows as 0, field initialization and the constructor body can be told apart. - CI4 — The measurement reads no environment-dependent data: duration, memory address, identity hash, and path are not written. The only things printed are order and value.
// Base.java — the course's core pair and the setup trace
import java.util.*;
class Base {
static final List<String> LOG = new ArrayList<>();
static void log(String s) { LOG.add(s); }
static void dump() { LOG.forEach(System.out::println); LOG.clear(); }
String label = "base field";
Base() {
log("2. Base constructor | name() -> " + name());
log(" | describe() -> " + describe());
}
String name() { return "base method"; }
String describe() { return "base: " + label; }
}
class Sub extends Base {
String label = "sub field";
int count;
{ log("3. Sub field init | describe() -> " + describe()); }
Sub(int count) {
log("4. Sub ctor body | describe() -> " + describe());
this.count = count;
log("5. after assignment | describe() -> " + describe());
}
@Override String name() { return "sub method"; }
@Override String describe() { return "sub: " + label + " x" + count; }
}
class Fragile extends Base {
String label = "fragile field";
@Override String describe() { return "fragile: " + label.length(); }
}
Sub‘s constructor has no super() call written in it. When it is not written, the
compiler places one at the chain’s start itself; a constructor’s first job is always to run
the base class’s constructor. The chain does not end at Base either — it runs up to the root
class.
In What Order Does the Constructor Chain Run
// Chain.java — which type decides while an object is under construction
public class Chain {
public static void main(String[] args) {
Base.log("1. new Sub(3) called");
Base d = new Sub(3);
Base.log("6. setup done | describe() -> " + d.describe());
System.out.println("declared type Base, runtime type "
+ d.getClass().getSimpleName());
System.out.println();
Base.dump();
System.out.println();
Fragile k = null;
try {
k = new Fragile();
} catch (NullPointerException e) {
System.out.println("fragile setup : " + e.getClass().getSimpleName());
}
System.out.println("delivered object: " + k);
}
}
declared type Base, runtime type Sub
1. new Sub(3) called
2. Base constructor | name() -> sub method
| describe() -> sub: null x0
3. Sub field init | describe() -> sub: sub field x0
4. Sub ctor body | describe() -> sub: sub field x0
5. after assignment | describe() -> sub: sub field x3
6. setup done | describe() -> sub: sub field x3
fragile setup : NullPointerException
delivered object: null
The order is four steps and runs in one direction: base constructor, sub field
initializers, sub constructor body, delivery. Even though new Sub(3) is what got written,
the first body to run is Base‘s constructor. This is inheritance’s counterpart on the
constructor side — an object comes into being only once every layer above its own class’s
layer has already been set up.
The second line is this lesson’s measurement. Base’s constructor writes name() and gets
sub method back. The one writing the call is Base, the one running is Sub‘s version:
an overridden method’s decision is made by the runtime type, and this rule is not
suspended even in the middle of the constructor chain. Even while the object is only half
built, its real class is already settled.
The third line is what that costs. The describe() call at that same point gives sub: null x0. Sub.describe() reads its own label field, but that field has not been assigned yet:
field initializers run after the base constructor finishes. So the runtime type has chosen
the correct method, and the chosen method is looking at a state that does not exist yet. The
two decisions come from separate sides, and a gap remains between them.
The gap’s width is read between the third and fifth lines. On the third line, label is
assigned, count is still 0; by the fifth, both are in place. Because count is assigned
in the constructor body (CI3), the two stages can be told apart: values given at
declaration sit in one place, those given in the constructor body settle later. Someone
reading the source sees label and count as two fields of the same class; setup order
scatters them across three separate moments.
The Cost of a Half-Built Object
The output’s last two lines are the gap’s real cost. Fragile does the same job as Sub,
with one difference — the method it overrides not only reads the field but calls on it too.
The result is a NullPointerException, and where it is born matters: the exception falls
inside Fragile.describe(), but the line calling that method sits in Base’s constructor.
The class writing the defect and the class where it shows are not the same.
The last line measures the consequence: the k reference is null. Since the constructor
never completed, new produced no value at all. A half-built object is not delivered, but
before it was delivered, a method already ran on it — the subclass’s method at that.
The rule that follows is this course’s first concrete contract: a constructor should not call a method that can be overridden. When it does, the code it writes runs the subclass’s version, and whoever wrote the subclass wrote it assuming its fields were already assigned. Each side is internally consistent; the defect is born exactly where the two meet, and it is invisible looking at either the base class’s source or the subclass’s alone.
A Bounding Measurement: No Trap in a Private or Final Method
If the rule held everywhere, it would not explain anything. There are two situations where the trap does not arise, and both come from the same reason: for those methods, there is no overriding in play at all.
- CI5 — The three methods in the boundary measurement carry the same body shape and differ
only in their modifiers: one private, one
final, one unmarked. In the subclass, all three have a counterpart written under the same name, except thefinalone — that one cannot be written.
// Boundary.java — no trap arises in a private or final method
public class Boundary {
public static void main(String[] args) {
BoundaryBase s = new BoundarySub();
System.out.println("class of constructed object : " + s.getClass().getSimpleName());
}
}
class BoundaryBase {
BoundaryBase() {
System.out.println("from constructor restricted(): " + restricted());
System.out.println("from constructor fixed() : " + fixed());
System.out.println("from constructor open() : " + open());
}
private String restricted() { return "base restricted"; }
final String fixed() { return "base fixed"; }
String open() { return "base open"; }
}
class BoundarySub extends BoundaryBase {
String label = "sub field";
private String restricted() { return "sub restricted: " + label; }
@Override String open() { return "sub open: " + label; }
}
from constructor restricted(): base restricted from constructor fixed() : base fixed from constructor open() : sub open: null class of constructed object : BoundarySub
Three lines, three separate sides. The constructed object’s class is the same in all three — the fourth line says so — but only the last one takes its answer from the runtime type.
The restricted() call gives base restricted. A private method is never inherited by a
subclass at all; the same-named method in the subclass is not its override, it is a separate
method. With no overridden version at all, the call’s only destination is BoundaryBase’s
own method — the runtime type has nothing to add.
The fixed() call gives base fixed. A final method cannot be overridden; a version
under the same signature written inside BoundarySub would have been rejected by the
compiler. With no other version to be found in the subclass, the only place the call can go
is the base class. The restriction itself is measured separately later in this course; what
shows here is its consequence during setup.
The open() call, in turn, gives sub open: null. An unmarked method can be overridden,
has been overridden, and the runtime type decides — followed by the familiar null.
The boundary’s meaning is this: whether a call made from a constructor is dangerous cannot be
read from how the call is written. All three lines are written the same way. What makes the
difference is the method’s modifier — that is, how much the base class leaves open to the
subclass. Declaring a helper method meant to be called from a constructor as private or
final turns the trap into not a warning, but a rule of the language.
Where Does the Call Bind in the Class File
The three calls were seen to be indistinguishable in the source. Are they distinguishable in
the class file? For the measurement, BoundaryBase is compiled and the call instructions in
its constructor’s body are read, along with the modifiers of those calls’ targets.
- CI6 — The measurement compiles only the
BoundaryBaseclass; the subclass is not even present. What is read is not a run, it is the file the compiler produces for a single class.
// Gauge.java — reads the class file: the calls in a constructor and their targets' modifiers
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.*;
import java.lang.reflect.AccessFlag;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;
class Gauge {
record Call(String name, String instruction, String target, String modifier, boolean overridable) {}
static List<Call> constructorCalls(String source, String className) throws Exception {
Path d = Files.createTempDirectory("gauge");
Path f = d.resolve(className + ".java");
Files.writeString(f, source);
PrintWriter sink = new PrintWriter(Writer.nullWriter());
if (ToolProvider.findFirst("javac").orElseThrow()
.run(sink, sink, "-d", d.toString(), f.toString()) != 0)
throw new IllegalStateException("did not compile: " + className);
ClassModel cm = ClassFile.of().parse(d.resolve(className + ".class"));
Map<String, String> modifiers = new HashMap<>();
for (MethodModel m : cm.methods()) {
var flags = m.flags();
modifiers.put(m.methodName().stringValue(),
flags.has(AccessFlag.PRIVATE) ? "private"
: flags.has(AccessFlag.FINAL) ? "final" : "-");
}
List<Call> calls = new ArrayList<>();
for (MethodModel m : cm.methods()) {
if (!m.methodName().stringValue().equals("<init>") || m.code().isEmpty()) continue;
for (CodeElement e : m.code().get())
if (e instanceof InvokeInstruction iv) {
String name = iv.name().stringValue();
String target = shortName(iv.owner().asInternalName());
String mod = target.equals(className) ? modifiers.getOrDefault(name, "-") : "-";
calls.add(new Call(name, iv.opcode().name().toLowerCase(Locale.ROOT),
target, mod, target.equals(className) && mod.equals("-")));
}
}
return calls;
}
static String shortName(String internal) { return internal.substring(internal.lastIndexOf('/') + 1); }
}
// Dispatch.java — which instruction do the calls in a constructor bind to in the class file
public class Dispatch {
static final String SOURCE = """
class BoundaryBase {
BoundaryBase() { restricted(); fixed(); open(); }
private String restricted() { return "base restricted"; }
final String fixed() { return "base fixed"; }
String open() { return "base open"; }
}
""";
public static void main(String[] args) throws Exception {
System.out.printf("%-11s %-14s %-13s %-10s %s%n",
"call", "instr", "target", "modifier", "overridable");
for (Gauge.Call c : Gauge.constructorCalls(SOURCE, "BoundaryBase"))
System.out.printf("%-11s %-14s %-13s %-10s %s%n",
c.name(), c.instruction(), c.target(), c.modifier(), c.overridable() ? "yes" : "no");
}
}
call instr target modifier overridable <init> invokespecial Object - no restricted invokevirtual BoundaryBase private no fixed invokevirtual BoundaryBase final no open invokevirtual BoundaryBase - yes
The first line is a call never written in the source. BoundaryBase‘s constructor has no
super() written in it, and yet an Object constructor call sits in the class file, as the
body’s first instruction. The claim that the constructor chain runs top to bottom is read
right here: the chain’s link was fastened by the compiler.
The real observation sits in the remaining three lines. All three calls compile to the same
instruction. The difference that could not be told apart in the source cannot be told apart
at the call site either: the instruction produced for restricted, fixed, and open is
byte-for-byte identical, and all three write their target as BoundaryBase. The call site
alone cannot say which one might reach a subclass.
The difference sits in the target’s modifier. A private method is not inherited, a final
method cannot be overridden; in neither case is there a second implementation the virtual
machine could look up, so dynamic dispatch finds only one candidate even though it runs. Only
the unmarked method can have a second candidate — exactly what happens in the Boundary run.
This is the course’s second claim, in its first instance: the difference is invisible in syntax. Now one step further: the difference is not visible where the call is written either. Which side the decision will come from is written not in the call, but in the declaration of the thing being called.
Summary
- A name carries two types: the declared type the compiler sees, and the runtime type that is the object’s real class. Every measurement in this course asks which one a decision comes from.
- The constructor chain runs top to bottom: base constructor, the subclass’s field
initializers, the subclass constructor’s body, then delivery. Even when
super()is not written, it sits in the class file and is the constructor’s first instruction. - An overridden method called from a constructor has its decision made by the runtime
type:
Base’s constructor writesname(), gets backsub method. - The chosen method looks at fields not yet assigned: at that same point,
describe()returnssub: null x0, because field initializers run only after the base constructor finishes. - When a field is not just read but called on, setup falls with a
NullPointerExceptionandnewproduces no value at all: the reference staysnull, and the half-built object is never delivered. - The trap does not arise in a private or
finalmethod: all three calls compile to the same instruction in the class file and write the same target; the difference is carried not by the call site, but by what is called.
Next Step
In this lesson, every member between Base and Sub was open to the other; which method
could be seen was never asked. Yet in the boundary measurement, a modifier entered the picture
for the first time: a method marked private was not inherited by the subclass at all, and
left the decision to the declared type. What that single word closes off was not measured. The
next lesson asks who a name is seen from: four visibility levels are crossed with four
call positions, and how many of the sixteen pairs compile is counted. The measurement’s second
half shows where visibility lives — it sits as a flag in the class file, while at runtime the
member keeps standing exactly where it was.
To keep your progress and take notes, Log in
My notes
Log in to take notes.