Layers of Logic

7.2

Abstraction, Polymorphism, Abstract Classes, and Interfaces

Program to a shared contract while each object keeps its own implementation.

Core30 min read4 exercises
01

Previously on

Section 7.1 made AirUnit a subtype of Unit. That gave the child shared state and behaviour.

This lesson uses the type relationship itself. One variable will hold several kinds of unit, and each object will answer the same request differently.

02

The problem

A report must count deployable units. Each concrete type uses a different rule.

static boolean isDeployable(Object value) {
    if (value instanceof GroundUnit) {
        GroundUnit unit = (GroundUnit) value;
        return unit.getReadiness() >= 60;
    }
    if (value instanceof AirUnit) {
        AirUnit unit = (AirUnit) value;
        return unit.getReadiness() >= 60 && unit.isWeatherClear();
    }
    if (value instanceof NavalUnit) {
        NavalUnit unit = (NavalUnit) value;
        return unit.getReadiness() >= 50;
    }
    return false;
}

Every new unit type requires another branch. Every report must know every concrete class.

The decision belongs with the object that owns the required state. The report needs one question: isDeployable().

03

The idea

Put the common operation on the parent type

abstract class Unit {
    private final String name;
    private final int readiness;

    Unit(String name, int readiness) {
        this.name = name;
        this.readiness = readiness;
    }

    String getName() {
        return name;
    }

    int getReadiness() {
        return readiness;
    }

    abstract boolean isDeployable();
}

Unit defines shared state and the required operation. It does not invent one deployability rule that fits every subtype.

The subclasses complete the missing method:

class GroundUnit extends Unit {
    GroundUnit(String name, int readiness) {
        super(name, readiness);
    }

    @Override
    boolean isDeployable() {
        return getReadiness() >= 60;
    }
}

class AirUnit extends Unit {
    private final boolean weatherClear;

    AirUnit(String name, int readiness, boolean weatherClear) {
        super(name, readiness);
        this.weatherClear = weatherClear;
    }

    @Override
    boolean isDeployable() {
        return getReadiness() >= 60 && weatherClear;
    }

    boolean isWeatherClear() {
        return weatherClear;
    }
}

class NavalUnit extends Unit {
    NavalUnit(String name, int readiness) {
        super(name, readiness);
    }

    @Override
    boolean isDeployable() {
        return getReadiness() >= 50;
    }
}

Now the report depends only on Unit:

static int countDeployable(Unit[] units) {
    int count = 0;
    for (Unit unit : units) {
        if (unit.isDeployable()) {
            count++;
        }
    }
    return count;
}

The array may contain all three concrete classes:

Unit[] units = {
    new GroundUnit("Atlas", 70),
    new AirUnit("Kite", 80, false),
    new NavalUnit("Tide", 55)
};

System.out.println(countDeployable(units)); // 2

Each element variable has declared type Unit. Each referenced object retains its real class.

The expression unit.isDeployable() has one source form. Different method bodies run for different objects. That is polymorphism.

Abstraction separates the request from its implementation

The report knows what it can ask: whether a unit is deployable. It does not know how each type decides.

That separation is abstraction. A useful abstraction exposes the operation a client needs and hides details the client should not coordinate.

Encapsulation and abstraction are related but not identical:

EncapsulationAbstraction
Main questionWho may access or change this state?What operation should the client see?
Typical toolsprivate fields and controlled methodsparent types, abstract methods, and interfaces
Benefitpreserves invariantslets callers ignore implementation choice
04

Under the hood

Going deeper

Reference type and object type do different jobs

Unit unit = new AirUnit("Kite", 80, true);

Unit is the variable’s declared type. AirUnit is the object’s run-time class.

The compiler uses the declared type to check which members can be called:

unit.getName();       // allowed: Unit declares it
unit.isDeployable();  // allowed: Unit declares it
// unit.isWeatherClear(); // not allowed if Unit has no such method

The JVM uses the receiver object’s class when dispatching an overridden instance method. Therefore unit.isDeployable() runs AirUnit.isDeployable().

This division protects type safety. The compiler guarantees that every possible Unit supports the call. Run-time dispatch preserves specialised behaviour.

Upcasting is safe; downcasting needs proof

Assigning a child reference to a parent variable is upcasting:

AirUnit air = new AirUnit("Kite", 80, true);
Unit unit = air;

No explicit cast is required. Every AirUnit satisfies the Unit contract.

The opposite direction is a downcast:

Unit unit = new GroundUnit("Atlas", 70);
AirUnit air = (AirUnit) unit; // ClassCastException at run time

The declared type alone cannot prove that the object is an AirUnit.

Use instanceof when a concrete operation is genuinely required:

if (unit instanceof AirUnit air) {
    System.out.println(air.isWeatherClear());
}

Pattern matching both checks the run-time type and creates the narrowed variable.

If a method contains many such branches, first ask whether the shared abstraction needs another operation.

Overriding has exact rules

An override replaces an inherited instance-method implementation for the same signature.

Use @Override. It asks the compiler to verify your intention and catches misspellings or wrong parameter types.

A valid override follows these rules:

  • The name and parameter types match the inherited method.
  • The return type is the same or a permitted covariant subtype.
  • Access is not made more restrictive.
  • A new checked exception is not broader than the inherited declaration allows.
  • The inherited method is not final.

Private methods are not visible to subclasses, so a same-named child method does not override them.

Static methods belong to classes and are hidden, not overridden. Their selection uses the declared reference or class name.

class Parent {
    static void kind() { System.out.println("parent static"); }
    void speak()       { System.out.println("parent instance"); }
}

class Child extends Parent {
    static void kind() { System.out.println("child static"); }

    @Override
    void speak()       { System.out.println("child instance"); }
}

Parent value = new Child();
value.kind();  // parent static
value.speak(); // child instance

Prefer calling static methods with a class name, such as Parent.kind(). It makes the non-polymorphic choice visible.

Overloading and overriding can occur together

class Parent {
    void send(Object value) { System.out.println("parent object"); }
}

class Child extends Parent {
    @Override
    void send(Object value) { System.out.println("child object"); }

    void send(String value) { System.out.println("child string"); }
}

Parent receiver = new Child();
receiver.send("hello");

The output is child object.

First, the compiler performs overload selection using declared type Parent. It sees send(Object), not the child-only overload.

Then the JVM dispatches that selected signature using the real receiver. It finds Child.send(Object).

Use this order for any mixed question:

  1. Compiler chooses the signature.
  2. JVM chooses the override for that signature.

What abstract means

An abstract class cannot be instantiated directly:

// new Unit("unknown", 0); // Unit is abstract

It may contain constructors, fields, concrete methods, and abstract methods. Its constructor still runs as part of building a concrete subclass.

An abstract method declares a required signature without a body:

abstract boolean isDeployable();

A concrete subclass must implement every inherited abstract method. Otherwise, that subclass must also be declared abstract.

An abstract class does not need to contain an abstract method. Declaring it abstract can mean the type is useful as a base but incomplete as a direct object.

Interfaces describe roles across class families

Suppose units and employees can both produce a report. They do not belong in one implementation hierarchy.

interface Reportable {
    String report();
}

Classes from unrelated hierarchies can implement the same interface:

class AirUnit extends Unit implements Reportable {
    // Unit methods omitted

    @Override
    public String report() {
        return "air unit report";
    }
}

class Employee implements Reportable {
    @Override
    public String report() {
        return "employee report";
    }
}

The interface becomes a variable, parameter, or return type:

static void printReport(Reportable value) {
    System.out.println(value.report());
}

printReport needs the capability, not the concrete class or its inheritance tree.

An ordinary interface method is implicitly public abstract. Implementations must therefore be public.

Interface fields are constants. They are implicitly public static final, so interfaces cannot hold per-object instance state.

Modern interfaces may also declare default methods with bodies, static methods, and private helper methods. Default methods let an interface evolve shared behaviour without becoming a stateful superclass.

Abstract class or interface

Need Better starting point
Shared per-object fields and constructor rules abstract class
One substantial implementation for closely related classes abstract class
A role shared by unrelated classes interface
Several capabilities on one class interfaces
A stable dependency type for callers usually interface

A class extends one class and implements several interfaces:

class AirUnit extends Unit implements Reportable, Comparable<AirUnit> {
}

Interfaces can inherit from several interfaces. They avoid multiple inheritance of instance state.

Default methods can still conflict. If two unrelated interfaces provide the same default signature, the implementing class must resolve the conflict explicitly.

The bytecode keeps the decision open

Run javap -c on a small polymorphic example. A normal virtual call uses an instruction such as invokevirtual. An interface call commonly uses invokeinterface.

The class file contains a symbolic method reference. At run time, method resolution and dispatch find the implementation for the receiver’s class.

JVM implementations optimise this heavily. They may use dispatch tables, inline caches, or speculative inlining. Those are implementation techniques, not source-language guarantees.

The source-level rule remains enough for reasoning: the compiler validates the call against the declared type, then run-time dispatch selects the instance override.

05

What it costs

Polymorphism removes concrete-type branches when each object can own the operation. A vague parent type with unrelated methods creates a different form of coupling.

Abstract classes share implementation but commit descendants to one class hierarchy. Interfaces are more flexible but may require repeated implementation or separate collaborators.

Choose the smallest contract the caller needs. A narrow abstraction is easier to implement, test, and change.

06

Check yourself

Answer from memory before opening the explanation. Then compare your rule with the answer and revisit the example if they differ.

  1. A variable has declared type Unit and refers to an AirUnit. Which type controls available method calls, and which type selects an override?

    Show the answer

    The declared type Unit controls what the compiler allows at the call site. If the method is overridable, the run-time class AirUnit selects the implementation when the call executes.

  2. What is the central difference between overloading and overriding?

    Show the answer

    Overloading selects among different parameter lists at compile time. Overriding selects an implementation of the same instance-method signature at run time. Overload selection uses compile-time argument types. Override dispatch uses the receiver object's run-time class.

  3. When should a design use an interface rather than an abstract class?

    Show the answer

    Use an interface for a capability or role that unrelated class families can implement. Use an abstract class when closely related classes need shared per-object state, construction logic, or a substantial common implementation. A class can implement several interfaces but extend only one class.

  4. Why is repeated downcasting often a design warning?

    Show the answer

    The caller is recovering concrete types that the abstraction was meant to hide. If every branch asks what the object is, the shared contract may be missing an operation that each implementation should provide polymorphically.

07

Exercises

Write each solution in VS Code, predict its result, and then run the checks. Reading an example does not replace implementing it.

4 exercises100 pointsabout 110 minutes

The Layers of Logic VS Code extension runs the checks for exercises marked checked. For a manual exercise, run the program and compare its behaviour with the stated requirements and sample output.
A

Delete Every instanceof

Real work·30 min·25 points

checkedex-7-2-a

Rewrite a method so that adding a new type never touches it again.

The starting version works. It also has to be edited every time somebody adds a unit type, and so does every other method shaped like it in the program. Miss one and it silently ignores the new type.

Move each rule into the class it belongs to and watch the loop collapse to one line.

Then prove it worked. Add a fourth unit type with a completely different rule, and check whether countDeployable appears in your diff. If it does not, you have understood polymorphism. If it does, the rule is still sitting in the loop.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class NoInstanceof {
    Unit(String name, double readiness)
    Ground(String name, double readiness, int armour)
    Air(String name, double readiness, boolean weatherClear)
    Naval(String name, double readiness, int displacement)
    Space(String name, double readiness, int shielding)
    Unit: String getName(), double getReadiness(), boolean isDeployable()
    NoInstanceof: static int countDeployable(Unit[] units)
}

Thresholds: ground 60, air 60 and clear weather, naval 50, space 80. The tests define a sixth type of their own and hand it to countDeployable. The instanceof version in the starter gets the five known types right and gets that one wrong, which is the difference the exercise is about.

What your program must do

  • Move each rule into the class it belongs to
  • Rewrite countDeployable so it contains no instanceof and no casts
  • Give Space its rule without editing countDeployable at all
  • Confirm countDeployable never appears in your diff after the first rewrite
NoInstanceof.java
class Unit {
    private final String name;
    private final double readiness;

    Unit(String name, double readiness) {
        this.name = name;
        this.readiness = readiness;
    }

    String getName()      { return name; }
    double getReadiness() { return readiness; }

    // TODO: the rule most units follow
    boolean isDeployable() { return false; }
}

class Ground extends Unit {
    private final int armour;
    Ground(String name, double readiness, int armour) { super(name, readiness); this.armour = armour; }
    int getArmour() { return armour; }
    // TODO: does ground differ from Unit, or is there nothing to write here?
}

class Air extends Unit {
    private final boolean weatherClear;
    Air(String name, double readiness, boolean weatherClear) { super(name, readiness); this.weatherClear = weatherClear; }
    boolean isWeatherClear() { return weatherClear; }
    // TODO: air needs the readiness AND the weather
}

class Naval extends Unit {
    private final int displacement;
    Naval(String name, double readiness, int displacement) { super(name, readiness); this.displacement = displacement; }
    int getDisplacement() { return displacement; }
    // TODO: naval sails at 50
}

// The fourth type. It arrived after countDeployable was written, which is the point.
class Space extends Unit {
    private final int shielding;
    Space(String name, double readiness, int shielding) { super(name, readiness); this.shielding = shielding; }
    int getShielding() { return shielding; }
    // TODO: space needs 80
}

public class NoInstanceof {

    // The version to delete. It is correct today and it has to be edited
    // every time somebody adds a type. Note that it does not know about Space.
    static int countDeployable(Unit[] units) {
        int ready = 0;
        for (Unit u : units) {
            if (u instanceof Ground) {
                if (u.getReadiness() >= 60) ready++;
            } else if (u instanceof Air) {
                Air a = (Air) u;
                if (u.getReadiness() >= 60 && a.isWeatherClear()) ready++;
            } else if (u instanceof Naval) {
                if (u.getReadiness() >= 50) ready++;
            }
        }
        return ready;
    }

    public static void main(String[] args) {
        Unit[] units = {
            new Ground("Anvil", 74, 3),
            new Air("Kestrel", 88, false),
            new Naval("Trident", 55, 9000),
            new Space("Orbiter", 91, 12),
        };
        System.out.println(countDeployable(units) + " of " + units.length + " deployable");
    }
}
Hint 1
Give Unit an isDeployable() with the most common rule, then override it only in the classes that differ. Ground matches the common rule, so ground overrides nothing.
Hint 2
The loop becomes for (Unit u : units) if (u.isDeployable()) ready++;. Everything else moved into the classes, which is where it was always describing something about.
Hint 3almost the answer
The last requirement is the whole exercise. Once the loop asks each unit, adding Space is a rule in Space and nothing else. If countDeployable shows up in your diff again, a rule is still sitting in the loop.
What this is really testing

Whether you can turn a chain of type checks into a polymorphic method. This is the single most valuable refactoring in object oriented code, and the payoff is measurable, because you add a type and the loop does not change.

B

See the Two invokes

Hard·30 min·30 points

checkedex-7-2-b

Back in Section 1.2 you ran javap on a Hello World and were told to keep the output, because it would matter twice. This is the first time.

Compile this and run javap -c on it. Look only at main.

You will see two different instructions. One names a method exactly, including its parameter types. The other names only a method name and a class that is not even the class of the object involved.

That difference is the entire answer to “what is the difference between overloading and overriding”, and you can now point at it in a file rather than reciting it.

Write your explanation in your own words. Two sentences is enough, and it should mention who decided and when.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Invokes {
    static String overriddenCallInstruction()
    static String classRecordedByThatInstruction()
    static String overloadedCallInstruction()
    static String intCallDescriptor()
    static String doubleCallDescriptor()
    static boolean overloadChosenByCompiler()
    static boolean overrideChosenByCompiler()
    static String predictedForChar()
    static String predictedForLong()
    static String predictedForInteger()
    static String predictedForNull()
}

The first seven are what you read in javap. Descriptors go in exactly the form javap prints them, such as (I)V. Whitespace and case are ignored, nothing else is. The four predictions are which pick overload the compiler chooses, answered as int, double or Object. Do not change over or pick, because the questions are about those exact methods.

What your program must do

  • Compile the file and run javap -c on the class, reading only main
  • Record the instruction and the class name on the overridden call
  • Record the instruction and both descriptors on the overloaded calls
  • Predict which pick overload is chosen for char, long, Integer and null, then run it and check
Invokes.java
class Unit    { boolean deployable() { return true; } }
class AirUnit extends Unit { @Override boolean deployable() { return false; } }

public class Invokes {

    // The two the bytecode question is about. Leave them alone.
    static void over(int x)    { }
    static void over(double x) { }

    // The three the prediction questions are about. Leave these alone too.
    static String pick(int x)    { return "int"; }
    static String pick(double x) { return "double"; }
    static String pick(Object x) { return "Object"; }

    // ---- what javap showed ----
    // Compile this file, then run: javap -c Invokes
    // Read only the main method. Fill in what you see.

    // TODO: the instruction on the u.deployable() line
    static String overriddenCallInstruction() { return "?"; }

    // TODO: the class name written beside it. Read it carefully before you answer.
    static String classRecordedByThatInstruction() { return "?"; }

    // TODO: the instruction on both over(...) lines
    static String overloadedCallInstruction() { return "?"; }

    // TODO: the descriptor beside over(5), in the form javap prints it
    static String intCallDescriptor() { return "?"; }

    // TODO: the descriptor beside over(5.0)
    static String doubleCallDescriptor() { return "?"; }

    // TODO: had the compiler already picked which over() runs?
    static boolean overloadChosenByCompiler() { return false; }

    // TODO: had it already picked which deployable() runs?
    static boolean overrideChosenByCompiler() { return true; }

    // ---- predictions, from the same rule ----
    // Which pick() does the compiler choose for each of these?
    // Answer first, then run main and check.

    // TODO
    static String predictedForChar() { return "?"; }

    // TODO
    static String predictedForLong() { return "?"; }

    // TODO
    static String predictedForInteger() { return "?"; }

    // TODO
    static String predictedForNull() { return "?"; }

    public static void main(String[] args) {
        Unit u = new AirUnit();
        System.out.println(u.deployable());

        over(5);
        over(5.0);

        char c = 'x';
        long millis = 5L;
        Integer boxed = 7;
        System.out.println("char    -> " + pick(c));
        System.out.println("long    -> " + pick(millis));
        System.out.println("Integer -> " + pick(boxed));
        System.out.println("null    -> " + pick(null));
    }
}
Hint 1
javap -c Invokes in the same folder as the class file. Read the lines between public static void main and the closing brace, and nothing else.
Hint 2
On the overridden call the class name is the surprise. The object is an AirUnit and the bytecode names Unit, the declared type of the variable. The compiler wrote down a name and left the choice for later.
Hint 3almost the answer
For the predictions there is one rule: the compiler tries widening a primitive first, then boxing, then varargs. char widens to int. long cannot narrow to int, so it widens to double. An Integer already is an Object, so nothing has to be converted at all.
What this is really testing

Whether you can prove the compile-time versus run-time difference rather than repeat it. You were promised this proof in Section 1.2, and this is you collecting on it.

C

Abstract Class or Interface

Real work·30 min·25 points

checkedex-7-2-c

Four things to model. Two of them are interfaces, one is an abstract class, and one could arguably be either.

Decide each one and write the reason down before you write any code. The deciding question is almost always the same: does this need to hold data that differs from object to object?

The Crate is the part that proves your design. It is not a vehicle in any sense, and it still needs to be tracked. If your design cannot handle that without duplication, you have used inheritance where you needed an interface.

Finish by writing a method that takes a Trackable and passing it both a Plane and a Crate. Two completely unrelated classes flowing through one method is the thing interfaces are for.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class Choose {
    Vehicle(String name, double fuelLevel): String getName(), double getFuelLevel(), void refuel(double litres)
    Flyable: void takeOff(), void land(), boolean isAirborne()
    Trackable: String position()
    Repairable: void takeDamage(), void repair(), boolean isDamaged()
    Plane(String name, double fuelLevel)
    Crate(String id, String position): String getId()
    Choose: static String[] positionsOf(Trackable[] items)
    Choose: static boolean vehicleCanBeAnInterface(), interfaceFieldsAreShared(), interfaceCanHoldPerObjectState(), crateIsAVehicle()
}

Which of the four are interfaces and which is a class is the exercise, so that is what the tests check first. A new Plane starts on the ground and undamaged. refuel rejects a negative amount with IllegalArgumentException. A plane's position must differ between airborne and grounded, and what the two strings say is up to you. One test implements Trackable itself and hands it to positionsOf.

What your program must do

  • Decide abstract class or interface for all four, with a written reason for each
  • Implement Plane and Crate, with Crate not a Vehicle in any sense
  • Write positionsOf to take anything Trackable, and pass it both a Plane and a Crate
  • Answer the four reason methods, and be able to say why Vehicle cannot be an interface
Choose.java
// You are modelling a fleet. Decide abstract class or interface for each,
// and implement all four.
//
//  Vehicle      - every vehicle has a name and a fuel level, and can refuel
//  Flyable      - can take off and land. Applies to planes AND to some drones.
//  Trackable    - reports a position. Applies to vehicles AND to cargo crates.
//  Repairable   - can be damaged and repaired. Applies to almost everything.
//
// Then build: Plane (a Vehicle that is Flyable, Trackable, Repairable)
//             Crate (not a Vehicle at all, and still Trackable)
//
// Decide each one and write the reason down before you write any code.

// TODO: class or interface?
abstract class Vehicle {
    // TODO: name, fuel level, a constructor, getName, getFuelLevel, refuel
}

// TODO: class or interface?
interface Flyable {
    // TODO: takeOff, land, isAirborne
}

// TODO: class or interface?
interface Trackable {
    // TODO: position
}

// TODO: class or interface?
interface Repairable {
    // TODO: takeDamage, repair, isDamaged
}

// TODO
class Plane {
}

// TODO: what must Crate NOT extend?
class Crate {
}

public class Choose {

    // TODO: accept anything trackable and return each position, in order
    static String[] positionsOf(Trackable[] items) {
        return new String[0];
    }

    // ---- the reasons, written down ----

    // TODO: could Vehicle have been an interface?
    static boolean vehicleCanBeAnInterface() { return true; }

    // TODO: is a field declared in an interface one value shared by everybody?
    static boolean interfaceFieldsAreShared() { return false; }

    // TODO: can an interface hold data that differs from object to object?
    static boolean interfaceCanHoldPerObjectState() { return true; }

    // TODO: in your design, is a Crate a Vehicle?
    static boolean crateIsAVehicle() { return true; }

    public static void main(String[] args) {
        // TODO: build a Plane and a Crate, and pass both through positionsOf
    }
}
Hint 1
Ask one question each time: does this need per-object data? Vehicle needs a fuel level that differs per vehicle. The other three describe what a thing can do and hold nothing.
Hint 2
An interface cannot hold per-object state. Its fields are public static final whether you write those words or not, meaning one shared value that never changes. That rules it out for Vehicle.
Hint 3almost the answer
The Crate is what proves the design. It is not a Vehicle at all, and it is still Trackable. A method taking Trackable accepts both, which is exactly the thing a single-parent hierarchy cannot give you.
What this is really testing

Whether you can pick between the two for a real design instead of reciting a table. The deciding question is almost always about shared state.

D

The Override That Was Not

Real work·20 min·20 points

checkedex-7-2-d

Three methods in a subclass, and none of them do what they appear to do.

Predict all three printed lines before running. Two of the results are surprising, and the third is surprising in a different way.

Then add @Override to both instance methods and watch two silent bugs turn into two compile errors with line numbers. That transformation is the whole argument for writing the annotation every time, and it costs nothing.

The static method is the interesting leftover. Fix the other two and it still prints the parent’s version. Work out why, and you will understand something most Java developers get wrong when asked.

What to write

These exact signatures. The checks call them by name, so a different name or a different parameter type will not build.

class NotAnOverride {
    Unit: boolean isDeployable(double minReadiness), String describe(), static String kind()
    AirUnit extends Unit: the same three, with the two instance ones genuinely overriding
    NotAnOverride: the prediction and explanation methods
    static boolean predictedDeployableLine()
    static String predictedDescribeLine()
    static String predictedKindLine()
    static int overrideAnnotationErrors()
    static String kindIsChosenBy()
    static boolean staticMethodsCanBePolymorphic()
    static String hidingOrOverriding()
}

The three predictions are about the broken version as given, so answer them before you change anything. Then fix both instance methods so they really override, and leave the static one exactly as it is. AirUnit.describe returns "an air unit". kindIsChosenBy is "variable" or "object", hidingOrOverriding is "hiding" or "overriding", and both ignore case and surrounding spaces.

What your program must do

  • Predict all three printed lines before running anything
  • Add @Override to both instance methods and record how many errors appear
  • Fix both so they genuinely override, removing the versions that never did
  • Explain why the static one prints what it does, even after the other two are fixed
NotAnOverride.java
class Unit {
    boolean isDeployable(double minReadiness) { return true; }
    String describe() { return "a unit"; }
    static String kind() { return "unit"; }
}

class AirUnit extends Unit {
    boolean isDeployable(int minReadiness) { return false; }   // note the type
    String describ() { return "an air unit"; }                 // note the spelling
    static String kind() { return "air"; }
}

public class NotAnOverride {

    // ---- predictions about the BROKEN version above, before you change anything ----

    // TODO: what does System.out.println(u.isDeployable(60.0)) print?
    static boolean predictedDeployableLine() { return false; }

    // TODO: what does System.out.println(u.describe()) print?
    static String predictedDescribeLine() { return "?"; }

    // TODO: what does System.out.println(Unit.kind()) print, with u declared as a Unit?
    static String predictedKindLine() { return "?"; }

    // ---- now add @Override to both instance methods and count what the compiler says ----

    // TODO: how many errors appeared?
    static int overrideAnnotationErrors() { return 0; }

    // ---- then fix both, and look at the static one again ----

    // TODO: "variable" or "object"? Which one decides that Unit.kind runs?
    static String kindIsChosenBy() { return "?"; }

    // TODO
    static boolean staticMethodsCanBePolymorphic() { return true; }

    // TODO: "hiding" or "overriding"? What is AirUnit.kind doing to Unit.kind?
    static String hidingOrOverriding() { return "?"; }

    public static void main(String[] args) {
        Unit u = new AirUnit();
        System.out.println(u.isDeployable(60.0));
        System.out.println(u.describe());
        System.out.println(Unit.kind());
        System.out.println(AirUnit.kind());
    }
}
Hint 1
For each method ask: is the signature identical to the parent's? A different parameter type makes a new overload, not an override. A different name makes a completely unrelated method.
Hint 2
Adding @Override turns both silent bugs into compile errors with line numbers. That is the entire reason to write it every time.
Hint 3almost the answer
The static one is different. Static methods belong to the class, so there is no object to consult and the variable's type decides. u is declared as Unit, so Unit.kind() runs. This is called hiding, and it does not become polymorphic no matter what you do.
What this is really testing

Whether you can spot a broken override. A typo or a changed parameter creates a brand new method, the parent version keeps running, and nothing warns you unless you asked it to.

08

After the credits

Trace every polymorphic call in two stages:

compile time: is this call valid, and which signature is selected?
run time: which override implements that signature for this object?

That model covers parent variables, interfaces, abstract classes, arrays of mixed subtypes, and later collection APIs.

Threads you opened in this section