---
title: 'Inheritance and Overriding'
source: 'https://academia.sh/en/courses/java-object-model/inheritance-and-overriding'
course: 'Object-Oriented Java'
language: en
updated: '2026-08-17T18:09:41+00:00'
license: 'CC BY-SA 4.0'
---

# Inheritance and Overriding

Eight decisions are read on a single reference and the answer splits right down the middle: four come from the runtime type, four from the declared type. Redeclaring fields under the same name is not overriding, it is hiding; once the signature changes, overriding never arises at all and the decision switches sides.

For two lessons, one side of the decision was measured at a time. The **runtime type** picked
the overridden method called from a constructor; the **declared type** decided whether a
member was visible at all. The two were never set side by side, so the rule between them was
never asked either.

This lesson is the course's core measurement. A single reference is set up — declared type
`Base`, runtime type `Sub` — and eight separate language decisions are read through that
reference. For each decision, one question is asked: which type gave the result? The
measurement's oracle is the rig itself; since we deliberately separated the two types, every
answer's source is known from the start.

The Programming Paradigms course measured inheritance's two promises and how the subtype
contract gets violated; what was counted there was **contract violation**. What is counted
here is **which side the decision comes from**, and the measurement comes from an actual run.

## The Measurement Core

The core is made of three types: an interface (`Carrier`), a base class (`Base`), and a
subclass (`Sub`) extending it. `Sub` **overrides** `Base`'s method, and **redeclares** its
field under the same name — that the two are separate things is this lesson's second
measurement.

- **CI11** — Every member returns its own origin as text (`base field`, `sub method`); the
  returned value itself says which declaration it came from, no inference is needed.
- **CI12** — All eight decisions are read through the **same** reference; the only thing that
  changes is the kind of member being read.
- **CI13** — The measurement reads no environment-dependent data: identity hash, duration, and
  path are not written; identity, where needed, is compared with `==`.

```java
// Base.java — the course's core: an interface, a base class, a subclass extending it
interface Carrier {
    default String carry() { return "interface default"; }
}

class Base implements Carrier {
    String label = "base field";
    String name() { return "base method"; }
    String match(Base u) { return "Base.match(Base)"; }
    static String status() { return "base static"; }
    @Override public String toString() { return "Base object"; }
}

class Sub extends Base {
    String label = "sub field";
    @Override String name() { return "sub method"; }
    String match(Sub a) { return "Sub.match(Sub)"; }
    static String status() { return "sub static"; }
    @Override public String carry() { return "sub override"; }
    @Override public String toString() { return "Sub object"; }
}
```

Two measurements in this lesson read not a run but a **compile**: in one, whether a source is
accepted; in the other, the fields counted in the class file produced. Both come from the same
helper.

```java
// Compile.java — compiles a source; gives either the compile result or a class's fields
import java.io.PrintWriter;
import java.io.Writer;
import java.lang.classfile.*;
import java.nio.file.*;
import java.util.*;
import java.util.spi.ToolProvider;

class Compile {
    static Path compile(String source) throws Exception {
        Path d = Files.createTempDirectory("inherit");
        Path f = d.resolve("Trial.java");
        Files.writeString(f, source);
        PrintWriter sink = new PrintWriter(Writer.nullWriter());
        return ToolProvider.findFirst("javac").orElseThrow()
                .run(sink, sink, "-d", d.toString(), f.toString()) == 0 ? d : null;
    }

    static String outcome(String source) throws Exception {
        return compile(source) == null ? "not compiled" : "compiled";
    }

    static List<String> fields(String source, String className) throws Exception {
        Path d = compile(source);
        List<String> names = new ArrayList<>();
        for (FieldModel f : ClassFile.of().parse(d.resolve(className + ".class")).fields())
            names.add(f.fieldName().stringValue());
        return names;
    }
}
```

## Eight Decisions, One Reference

```java
// Decision.java — eight decisions on a single reference
public class Decision {
    static String pick(Base u) { return "Base signature"; }
    static String pick(Sub a) { return "Sub signature"; }

    static void report(String decision, String result, String decider) {
        System.out.printf("%-24s %-22s %s%n", decision, result, decider);
    }

    public static void main(String[] args) {
        Base d = new Sub();
        System.out.println("declared type Base, runtime type "
                + d.getClass().getSimpleName());
        System.out.println();
        System.out.printf("%-24s %-22s %s%n", "decision", "result", "decided by");
        report("overridden method", d.name(), "runtime type");
        report("interface default", d.carry(), "runtime type");
        report("toString conversion", d.toString(), "runtime type");
        report("field read", d.label, "declared type");
        report("overload pick", pick(d), "declared type");
        report("static method", Base.status(), "declared type");
        report("type test", String.valueOf(d instanceof Sub), "runtime type");
        report("downcast", ((Sub) d).label, "declared type (after cast)");
    }
}
```

```
declared type Base, runtime type Sub

decision                 result                 decided by
overridden method        sub method             runtime type
interface default        sub override           runtime type
toString conversion      Sub object             runtime type
field read               base field             declared type
overload pick            Base signature         declared type
static method            base static            declared type
type test                true                   runtime type
downcast                 sub field              declared type (after cast)
```

**Four** of eight decisions come from the runtime type, **four** from the declared type. The
sentence "Java is object-oriented, a call goes to the real object" explains only half of the
decisions.

The split is not arbitrary. The top three rows plus the seventh concern **behavior**: which
body runs, what the object actually is. These look at the runtime type, because the answer can
only be known once the object exists. The remaining four are **name resolution**: which field
gets read, which signature gets picked, which class's static method gets called. These end at
the compiler's desk, and the compiler has only the declared type in hand.

The rule fits in one sentence: **the behavior called looks at the runtime type, the name
picked looks at the declared type.** The second row is this rule's least visible instance — a
default method coming from an interface can be overridden too, and its decision also comes
from the runtime type.

The last row adds a fourth reading. `((Sub) d).label` gives **sub field**, even though the
object has been the same object all along. A downcast does not change the object, it changes
the **declared type**; it is a promise given to the compiler, and the promise's truth is
tested at runtime.

## Separating Overriding from Hiding

The fourth row is this lesson's quietest result. `d.name()` gives **sub method** while
`d.label` gives **base field**; both are written through the same `d`, with a two-character
difference. Redeclaring fields under the same name is not overriding; it is **hiding**. To see
what the hidden field is, two separate references are bound to a single object.

```java
// Hiding.java — the hidden field does not vanish
public class Hiding {
    public static void main(String[] args) {
        Base u = new Sub();
        Sub a = (Sub) u;
        System.out.println("are the two references the same object: " + (u == a));
        System.out.println("label via Base reference   : " + u.label);
        System.out.println("label via Sub reference    : " + a.label);
        System.out.println("name() via Base reference  : " + u.name());
        System.out.println("name() via Sub reference   : " + a.name());
        a.label = "written";
        System.out.println("u.label after writing a.label: " + u.label);
    }
}
```

```
are the two references the same object: true
label via Base reference   : base field
label via Sub reference    : sub field
name() via Base reference  : sub method
name() via Sub reference   : sub method
u.label after writing a.label: base field
```

The first line is the measurement's foundation: there is exactly one object. Yet that single
object gives **two separate answers** to the `label` question, and **one answer** to the
`name()` question. The overridden method replaces the one above it; the redeclared field does
not replace the one above it, **it sits on top of it.**

The last line is this in memory terms. After `a.label` is written to, `u.label` still says
**base field**. There are two separate places inside the same object, and writing one does not
change the other. The hidden field has not vanished; it has only become invisible from a
reference whose declared type is `Sub`.

That the two places are truly two places can also be read from the class file. The same pair
is compiled and the fields each class declares are listed.

```java
// Location.java — where the hidden field sits in the class file
public class Location {
    static final String SOURCE = """
        class Base { String label = "base field"; String name() { return "base method"; } }
        class Sub extends Base { String label = "sub field";
                                @Override String name() { return "sub method"; } }
        """;

    public static void main(String[] args) throws Exception {
        System.out.println("fields in Base's class file: " + Compile.fields(SOURCE, "Base"));
        System.out.println("fields in Sub's class file: " + Compile.fields(SOURCE, "Sub"));
    }
}
```

```
fields in Base's class file: [label]
fields in Sub's class file: [label]
```

Two separate class files, one `label` declaration in each. The overridden `name()` method
also has two bodies, but a call only ever goes to one of them; a field has no such choice —
both stay readable. Inheritance **replaces one with the other** for a method, and **leaves the
two standing side by side** for a field.

This is where the cost is read. A programmer writing a field under the same name as the one
above it in a subclass usually believes they have changed the field above. They have not —
both fields go on living, and which one is read is decided by looking at the declared type of
the reference on that line. The defect is invisible where it is written; it shows where it is
**read**, silently returning the wrong value.

## What the Compiler Catches and Misses

Overriding is not a declaration; it arises on its own whenever two signatures line up
exactly. For this reason, its **not** arising is automatic too, and silent. The `@Override`
mark exists precisely to break that silence: it makes the compiler answer, for the subclass,
the question "is this really an override?"

- **CI14** — In all five trials, the base class is byte-for-byte identical; the only thing
  that changes is the subclass's single-line declaration. What is measured is only the
  compile result.

```java
// Mark.java — what @Override catches, how far a signature can drift
public class Mark {
    static final String ROOT = """
        class A { String name() { return "base"; } A make() { return this; }
                  String match(A a) { return "A"; } }
        """;

    static final String[][] TRIALS = {
        { "unmarked, name misspelled", "class B extends A { String naem() { return \"sub\"; } }" },
        { "marked, name misspelled", "class B extends A { @Override String naem() { return \"sub\"; } }" },
        { "marked, return type narrowed", "class B extends A { @Override B make() { return this; } }" },
        { "marked, return type widened", "class B extends A { @Override Object make() { return this; } }" },
        { "marked, parameter type narrowed", "class B extends A { @Override String match(B b) { return \"B\"; } }" },
    };

    public static void main(String[] args) throws Exception {
        System.out.printf("%-38s %s%n", "subclass", "result");
        for (String[] d : TRIALS)
            System.out.printf("%-38s %s%n", d[0], Compile.outcome(ROOT + d[1] + "\n"));
    }
}
```

```
subclass                               result
unmarked, name misspelled              compiled
marked, name misspelled                not compiled
marked, return type narrowed           compiled
marked, return type widened            not compiled
marked, parameter type narrowed        not compiled
```

The first two rows carry the mark's whole value. A method with a misspelled name **compiles**
when no mark is placed: as far as the compiler is concerned there is no defect, a new method
has only been added to the subclass. The program runs, no warning is raised, and the base
class's version keeps getting called. The same source does **not compile** with `@Override`.
The mark adds no behavior; it only states an assumption to the compiler, and rejects the
source when that assumption does not hold.

The third and fourth rows measure how far a return type is allowed to drift. An overriding
method can **narrow** its return type — if the one above returns `A`, the one below can return
`B` — but cannot widen it. The reason is the subtype contract: when a caller expecting `A`
gets a `B`, the contract is not broken; when it gets an `Object`, it is. Narrowing strengthens
the promise given to the caller, widening weakens it.

The fifth row opens the door to the next section. When the parameter type is narrowed, the
marked source does **not compile**: there is no override in play at all. The narrowing that is
free in a return type is forbidden in a parameter — and the consequence of this is not only a
compile error.

## A Bounding Measurement: When the Signature Changes, the Decision Switches Sides

When `@Override` is not written, the same declaration compiles. What happens then? The
`match` pair added to the core is exactly this case: `Base` takes a `Base`, `Sub` takes a
`Sub`, the signatures do not line up, and so what exists is not overriding but **overloading**.

- **CI15** — In both calls the object is the same object and the argument is the same
  argument; the only thing that changes is the declared type of the reference the call is
  made through.

```java
// Signature.java — when the signature changes, overload replaces overriding
public class Signature {
    public static void main(String[] args) {
        Base d = new Sub();
        Sub real = new Sub();
        System.out.println("match via Base reference : " + d.match(real));
        System.out.println("match via Sub reference  : " + ((Sub) d).match(real));
        System.out.println("name() via Base reference: " + d.name());
        System.out.println("name() via Sub reference : " + ((Sub) d).name());
    }
}
```

```
match via Base reference : Base.match(Base)
match via Sub reference  : Sub.match(Sub)
name() via Base reference: sub method
name() via Sub reference : sub method
```

The four lines are read in pairs. The bottom two are overriding's signature: two separate
references, **one answer**. Changing the reference's declared type does not change the
result, because it is not the side giving the decision.

The top two, in turn, are overloading's signature: same object, same argument, **two separate
answers**. Two calls that could be written without changing a single character go to separate
bodies for no reason but the reference's declared type. The decision has switched sides.

This is what the boundary means. A programmer writing a method in a subclass usually believes
they are overriding; if a letter or a parameter type slips in the signature, overriding never
arises at all, and in its place comes a choice that never listens to the runtime type. The
program compiles, runs, and calls the base class's version. This is why `@Override` is not a
comment added to the source — it is the only way to freeze, at compile time, which side a
decision will come from.

## Summary

- Eight decisions are read on a single reference and the answer splits down the middle:
  **four** come from the runtime type, **four** from the declared type.
- The rule is single: **the behavior called** looks at the runtime type, **the name picked**
  looks at the declared type. A default method from an interface can be overridden too and
  falls into the first group.
- A downcast does not change the object, it changes the declared type: `((Sub) d).label` gives
  **sub field**, while the object has been the same object all along.
- Redeclaring fields under the same name is **hiding**: a single object gives two answers to
  the `label` question, and writing one does not change the other — the hidden field does not
  vanish.
- `@Override` adds no behavior, it states an assumption: a method with a misspelled name
  **compiles** unmarked, does **not compile** marked. The return type can be narrowed, not
  widened.
- When the signature does not line up, overriding never arises, it becomes overloading, and
  the decision switches sides: `match` gives two separate answers from two references while
  `name()` gives one.

## Next Step

In this lesson, `Base` carried a body: every method had an implementation, and the subclass
overrode them if it chose to. The default method from the interface entered the table as one
row too, but what the interface itself is for was never asked. The next lesson sets two tools
of abstraction side by side: a capability can be established with an abstract class as well as
an interface, one is bound to a single supertype, the other to many. The measurement looks at
the case where two interfaces give the same default method — in how many cases does the
compiler refuse to decide, in how many does one side silently win — and shows why a capability
carrying state cannot be established with an interface.
