---
title: 'Static Members and Initialization Blocks'
source: 'https://academia.sh/en/courses/java-object-model/static-members-and-initialization-blocks'
course: 'Object-Oriented Java'
language: en
updated: '2026-08-17T18:09:41+00:00'
license: 'CC BY-SA 4.0'
---

# Static Members and Initialization Blocks

A static method is not overridden, it is hidden: the same call spelling is picked from the declared type, and a static call made through a null reference even works. The class-level state's single copy, which access triggers class initialization, and the case where the same line goes to two separate sides are measured.

The previous lesson set two abstraction tools side by side: the abstract class worked with
single inheritance, the interface with multiple, and when two independent interfaces gave the
same default, the compiler refused to decide. What the two tools shared was that both worked
at the **instance** level. Every method called belonged to an object, so the decision mostly
came from the runtime type.

This lesson's subject is members belonging to the class itself: **the static method**, the
**class variable**, and the **initialization block**. Behind a method called without an
object, there is no runtime type; what remains is only the name the compiler sees. There are
three questions: does the same line go to different sides for an instance method versus a
static method, how many copies does class-level state carry, and which access triggers a
class's initialization block?

## The Measurement Core

The course's core is the same pair: declared type `Base`, runtime type `Sub`. This lesson
adds two members to the pair — a class variable (`produced`) and an instance field (`turn`) —
but the existing members' behavior does not change.

- **CI21** — The two types are deliberately separated; the oracle is the rig itself. When
  `Base d = new Sub()` is written, which type an answer comes from is known before looking at
  the answer.
- **CI22** — Only members are added to the `Base`/`Sub` pair; the values the shared
  definition's members produce do not change.
- **CI23** — In the initialization measurement, every scenario runs in a **separate loader**;
  one scenario's initializing a class does not affect another.
- **CI24** — Initialization steps print their name when they run; order is a dump, not an
  inference.
- **CI25** — The only thing counted is which side the decision comes from. Instruction count
  and added steps are not counted in this course; the class file is read only to show where
  the decision froze, and the reading calls the compiler from within the program and parses
  the file it produces with the standard library.

```java
// Base.java — the course's core: instance method next to static method
class Base {
    static int produced = 0;
    final int turn;
    Base() { turn = ++produced; }
    String name() { return "base method"; }
    static String status() { return "base static"; }
}
```

```java
// Sub.java — subclass redeclaring the same two names
class Sub extends Base {
    @Override String name() { return "sub method"; }
    static String status() { return "sub static"; }
}
```

`name` carries an `@Override` mark, and the compiler accepts it: this is an **overriding**.
`status` carries no such mark — had it, compilation would stop. A static method cannot be
overridden; what happens when it is redeclared under the same name is called **hiding**, and
the behavior we saw hiding produce in fields repeats here for methods.

## Same Spelling, Two Sides

The first measurement puts five calls side by side. Three are made through a reference
declared `Base`, one through a reference declared `Sub`, and the last through a reference
bound to no object at all.

```java
// Side.java — which type does the same call spelling get picked from
public class Side {
    @SuppressWarnings("static-access")
    public static void main(String[] args) {
        Base d = new Sub();
        Sub a = new Sub();
        Base empty = null;
        System.out.printf("%-30s %-12s %s%n", "call written", "result", "decided by");
        System.out.printf("%-30s %-12s %s%n", "d.name()     [declared Base]", d.name(), "runtime type");
        System.out.printf("%-30s %-12s %s%n", "d.status()   [declared Base]", d.status(), "declared type");
        System.out.printf("%-30s %-12s %s%n", "a.name()     [declared Sub]", a.name(), "runtime type");
        System.out.printf("%-30s %-12s %s%n", "a.status()   [declared Sub]", a.status(), "declared type");
        System.out.printf("%-30s %-12s %s%n", "empty.status() [null ref]", empty.status(), "declared type");
        System.out.println();
        System.out.println("instance field -> d.turn " + d.turn + " | a.turn " + a.turn);
        System.out.println("class field -> Base.produced " + Base.produced + " | Sub.produced " + Sub.produced);
        Sub.produced = 99;
        System.out.println("after Sub.produced = 99, Base.produced: " + Base.produced);
    }
}
```

```
call written                   result       decided by
d.name()     [declared Base]   sub method   runtime type
d.status()   [declared Base]   base static  declared type
a.name()     [declared Sub]    sub method   runtime type
a.status()   [declared Sub]    sub static   declared type
empty.status() [null ref]      base static  declared type

instance field -> d.turn 1 | a.turn 2
class field -> Base.produced 2 | Sub.produced 2
after Sub.produced = 99, Base.produced: 99
```

The first two lines are written through the same `d` reference and give opposite answers.
`d.name()` returns **sub method**: the object is a `Sub`, and the method was picked from
there. `d.status()` returns **base static**: the object is still the same object, but the
pick was made from `d`'s declared type. The only difference between the two is that one of
the two declarations inside `Sub` starts with the word `static`.

The fifth line is proof of this reading. `empty` is bound to no object at all; even so, the
call `empty.status()` gives a result and raises no error. The reference is not even **read**.
A reference appearing in a static call's spelling is only a syntactic convenience; the
compiler takes that reference's **type**, and discards its value.

The bottom three lines answer the second question. Two objects were produced, and each
carries its own `turn` value: **1** and **2**. `produced`, in turn, gives the same number —
**2** — no matter which of the two names it is read through. The name written as
`Sub.produced` is not a separate counter; once `Sub.produced = 99` is written, `Base.produced`
also becomes 99. An instance field carries one copy per object; a class variable is **one**
single copy across the whole inheritance chain.

## The Proof in the Class File

Behavior has been measured; now where the decision froze can be looked at. The measurement
core compiles a source text, reads the class file it produces, and returns two things for
every call inside a method's body: the call's **owner** as written in the class file, and the
call's **kind**.

```java
// Gauge.java — a call's owner and kind as written in the class file
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.lang.classfile.instruction.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Gauge {
    record Call(String owner, String name, String kind) {}

    static Path compile(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);
        return d;
    }

    static List<Call> calls(String source, String className, String method) throws Exception {
        ClassModel cm = ClassFile.of().parse(compile(source, className).resolve(className + ".class"));
        List<Call> calls = new ArrayList<>();
        for (MethodModel m : cm.methods()) {
            if (!m.methodName().stringValue().equals(method) || m.code().isEmpty()) continue;
            for (CodeElement e : m.code().get())
                if (e instanceof InvokeInstruction iv)
                    calls.add(new Call(shortName(iv.owner().asInternalName()),
                            iv.name().stringValue(), kind(iv.opcode())));
        }
        return calls;
    }

    static String kind(Opcode o) {
        return switch (o) {
            case INVOKESTATIC -> "static call";
            case INVOKEVIRTUAL -> "virtual call";
            default -> "direct call";
        };
    }

    static String shortName(String internal) { return internal.substring(internal.lastIndexOf('/') + 1); }
}
```

```java
// Owner.java — what is the call's owner written as in the class file
public class Owner {
    static final String SOURCE = """
        class Base {
            String name() { return "base method"; }
            static String status() { return "base static"; }
        }
        class Sub extends Base {
            @Override String name() { return "sub method"; }
            static String status() { return "sub static"; }
        }
        class Caller {
            static String baseInstance(Base d) { return d.name(); }
            static String subInstance(Sub a) { return a.name(); }
            @SuppressWarnings("static-access")
            static String baseStatic(Base d) { return d.status(); }
            @SuppressWarnings("static-access")
            static String subStatic(Sub a) { return a.status(); }
        }
        """;

    public static void main(String[] args) throws Exception {
        System.out.printf("%-13s %-24s %s%n", "method", "call in class file", "call kind");
        for (String y : new String[] {"baseInstance", "subInstance", "baseStatic", "subStatic"})
            for (Gauge.Call c : Gauge.calls(SOURCE, "Caller", y))
                System.out.printf("%-13s %-24s %s%n", y, c.owner() + "." + c.name(), c.kind());
    }
}
```

```
method        call in class file       call kind
baseInstance  Base.name                virtual call
subInstance   Sub.name                 virtual call
baseStatic    Base.status              static call
subStatic     Sub.status               static call
```

In all four lines, the owner written is the **declared type**; this is the table's most
misread spot. The `baseInstance` method also has `Base` as its owner, and yet the method that
actually runs is `Sub`'s. The distinction is not in the owner, it is in the **kind**. In a
virtual call, the owner is only a starting point: the virtual machine re-resolves the call
starting from the object's real class, so the `Base.name` text written in the class file does
not decide the result. In a static call, there is no resolving; the owner **is** the result
itself, and `Sub.status` and `Base.status` are **two separate targets**.

This reduces the lesson's main measurement to one sentence: a static call's target is written
at compile time and has no place left to be changed at runtime.

## Which Access Triggers Initialization

Class-level state has to be set up somewhere, and that place is the **initialization block**.
When this block runs — not at program launch, but the first time the class is really needed,
and only once — was measured in the Java Fundamentals course and is not repeated here. The
question here is different: **which spelling** counts as being needed?

The measurement below runs four separate accesses in four separate loaders, so every scenario
starts from a clean slate. Initialization blocks print their name when they run.

```java
// Trigger.java — which access triggers class initialization
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Path;

public class Trigger {
    static final String SOURCE = """
        class Base {
            static { System.out.println("  Base initialized"); }
            static int counter = 0;
            static final int CEILING = 100;
        }
        class Sub extends Base {
            static { System.out.println("  Sub initialized"); }
            static int subCounter = 0;
        }
        class S1 { static void run() { System.out.println("  value read: " + Sub.counter); } }
        class S2 { static void run() { System.out.println("  value read: " + Base.CEILING); } }
        class S3 { static void run() { System.out.println("  value read: " + Sub.subCounter); } }
        class S4 { static void run() { new Sub(); System.out.println("  a Sub was produced"); } }
        """;

    static void scenario(Path d, String driver, String spelling) throws Exception {
        System.out.println("access written: " + spelling);
        try (URLClassLoader cl = new URLClassLoader(new URL[] {d.toUri().toURL()}, null)) {
            var m = cl.loadClass(driver).getDeclaredMethod("run");
            m.setAccessible(true);
            m.invoke(null);
        }
        System.out.println();
    }

    public static void main(String[] args) throws Exception {
        Path d = Gauge.compile(SOURCE, "Base");
        scenario(d, "S1", "Sub.counter    [field declared in Base]");
        scenario(d, "S2", "Base.CEILING   [compile-time constant]");
        scenario(d, "S3", "Sub.subCounter [field declared in Sub]");
        scenario(d, "S4", "new Sub()");
    }
}
```

```
access written: Sub.counter    [field declared in Base]
  Base initialized
  value read: 0

access written: Base.CEILING   [compile-time constant]
  value read: 100

access written: Sub.subCounter [field declared in Sub]
  Base initialized
  Sub initialized
  value read: 0

access written: new Sub()
  Base initialized
  Sub initialized
  a Sub was produced
```

In the first scenario, the name written in the source is `Sub`, but the class initialized is
**`Base`**, and `Sub` is never initialized at all. The written name is not the trigger; the
class the field is **declared in** is the trigger, and that is found at compile time. The
third scenario uses the same spelling, but with a field declared inside `Sub`, and this time
both classes initialize, with the base class first. Same spelling, two separate outcomes; what
makes the difference is not the name itself, it is which class the name resolves to.

The second scenario goes further still: **no class initializes**, yet the value is read and
comes out correct. `CEILING` is a `static final` field whose value is a constant computable at
compile time; the compiler has written that value directly into the reading side, so `Base` is
never touched at runtime at all. Reading a class variable initializes the class; reading a
constant does not — and the two lines are indistinguishable in the source.

## A Bounding Measurement: The Same Line Goes to Two Sides

The measurements up to now called an instance method and a static method on **separate
lines**. The real defect is born once the two sit side by side: a single line, a single
reference, two separate decisions.

```java
// SideBySide.java — the same line goes to two separate sides
public class SideBySide {
    @SuppressWarnings("static-access")
    static String line(Base d) { return d.name() + " / " + d.status(); }

    @SuppressWarnings("static-access")
    public static void main(String[] args) {
        for (Base d : new Base[] {new Base(), new Sub()})
            System.out.printf("Base d = new %-6s -> %s%n", d.getClass().getSimpleName() + "()", line(d));
        Sub a = new Sub();
        System.out.printf("Sub a = new %-6s -> %s%n", "Sub()", a.name() + " / " + a.status());
    }
}
```

```
Base d = new Base() -> base method / base static
Base d = new Sub()  -> sub method / base static
Sub a = new Sub()  -> sub method / sub static
```

The `line` method never changed; the same body ran on all three lines. Yet the three results
give three separate combinations. In the first line the two answers agree, because the
declared type and the runtime type are the same. They agree in the third line too, for the
same reason. **The split is visible only on the second line** — and exactly there, a
programmer's expected reading breaks: the object is a `Sub`, yet the status information comes
from `Base`.

Two boundaries follow from this. First, there is no situation where the rule reverses; a
static member is always picked from the declared type. Second, and more important, **the
split cannot be observed in most programs**: as long as the declared type and the runtime type
are not separated, the two sides give the same answer and the difference never surfaces at
all. The day a subclass is written for a class, a line that had worked correctly up to that
day can silently change direction. The defect's cost is not paid where it is written, it is
paid where the subclass is **used** — and what shows up there is not an error, it is a
**wrong value**.

## Summary

- A static method is not overridden, it is **hidden**: `d.name()` gives **sub method** from
  the runtime type, `d.status()` gives **base static** from the declared type; the only
  difference between the two lines is the word `static` in the declaration.
- A static call never reads the reference's value at all; `empty.status()` made through a null
  reference gives a result and raises no error.
- An instance field carries one copy per object (`turn` gives 1 and 2), a class variable is
  single across the inheritance chain: once `Sub.produced = 99` is written, `Base.produced`
  becomes 99 too.
- In the class file, all four calls' owner is the declared type; the distinction is in the
  kind. In a virtual call the owner is a starting point, in a static call it is the result
  itself.
- Class initialization is triggered not by the written name, but by the class the field is
  **declared in**: `Sub.counter` initializes only `Base`, while `Base.CEILING` initializes no
  class at all.
- The split is visible only when the declared type and the runtime type are separated; in two
  of the three-line measurement, the two sides give the same answer and the difference cannot
  be observed.

## Next Step

In this lesson's last measurement, a value was read without ever triggering class
initialization: `CEILING` was a `static final` field, and its value had passed to the reading
side. That single word, `final`, showed up here only as a side effect; in fact it does three
separate jobs at once, and in all three it carries the decision to **compile time**. The same
word blocks reassigning a local variable, overriding a method, and extending a class. The next
lesson measures these three constraints separately, shows the consequence of a constant value
being embedded in the reading class, and bounds, with the same measurement, that a `final`
reference does not freeze the object — meaning it is not immutability.
