Skip to content

Lesson 02 · Inheritance & Polymorphism

Objectives

After this lesson you will be able to:

  • Use extends and super, and know what a subclass inherits.
  • Predict the full initialization order (statics and instances) across a hierarchy.
  • Declare and use abstract classes and methods.
  • Tell overriding from hiding and overloading, and predict polymorphic dispatch.
  • Differentiate object type from reference type.
  • Override Object methods (equals, hashCode, toString) under their contracts.
  • Use final (class, method, and variable), casting, and instanceof safely.

extends and super

A subclass extends exactly one superclass and inherits its accessible members. A subclass constructor implicitly calls super() (the no-arg superclass constructor) as its first action unless you call super(...) or this(...) explicitly.

java
class Animal {
    Animal(String name) { }         // no no-arg constructor!
}
class Dog extends Animal {
    Dog() { super("dog"); }         // REQUIRED — Animal has no no-arg constructor
}

Exam trap

If the superclass has no no-arg constructor, the subclass must call super(...) explicitly, or it won't compile (the implicit super() has nothing to call).

super also reaches into the parent at runtime: super.m() calls the parent's version of an overridden method, and super.field reads a hidden parent field. (super.super is illegal — you can only reach one level up.)

java
class Base { String greet() { return "hi"; } }
class Loud extends Base {
    @Override String greet() { return super.greet().toUpperCase() + "!"; }   // "HI!"
}

What a subclass inherits (and what it doesn't)

A subclass inherits every accessible, non-private member of its superclass — and not the rest:

MemberInherited?
public / protected methods & fieldsYes
package-private membersYes only if the subclass is in the same package
private membersNo — they exist in the object but aren't accessible by name
static membersYes (but they belong to the class, and are hidden not overridden)
ConstructorsNo — they aren't members; invoke the parent's via super(...)

Exam trap

Constructors are never inherited, and private members are not inherited. A protected member is accessible in a subclass in another package — but only through the subclass's own type/this, not through a superclass-typed reference.

Initialization order across a hierarchy

Building an object initializes the whole chain in a fixed order:

  1. Static fields and static blocks of each class, superclass before subclass, in source order — once, when each class is first loaded.
  2. For each new: the superclass instance field initializers and instance blocks, then the superclass constructor body.
  3. Then the subclass instance field initializers and blocks, then the subclass constructor body.
java
class Parent { { System.out.print("P-init "); } Parent() { System.out.print("P-ctor "); } }
class Child extends Parent { { System.out.print("C-init "); } Child() { System.out.print("C-ctor "); } }

new Child();   // prints:  P-init P-ctor C-init C-ctor

The subtle trap: the superclass constructor runs before the subclass's fields are initialized. If the super constructor calls an overridden method, it runs the subclass override while those fields are still at their defaults (null/0).

java
class Base {
    Base() { init(); }                       // calls an overridable method
    void init() { System.out.println("Base.init"); }
}
class Derived extends Base {
    String tag = "ready";
    @Override void init() { System.out.println("tag=" + tag); }
}

new Derived();          // prints: tag=null  — super ctor ran before tag was assigned

Exam trap

Calling an overridable method from a constructor is a classic bug: the override sees uninitialized subclass fields. The fix is to make such methods private, static, or final so they can't be overridden.

Abstract classes and methods

An abstract class cannot be instantiated — it exists to be extended. An abstract method has no body; any concrete subclass must override it. An abstract class may still have constructors (run via super(...)), fields, and concrete methods.

java
abstract class Shape {
    abstract double area();                 // no body — subclasses must implement
    double describe() { return area(); }    // concrete method may call the abstract one
}
// new Shape();                             // COMPILE ERROR — Shape is abstract

class Square extends Shape {
    final double side;
    Square(double side) { this.side = side; }
    @Override double area() { return side * side; }
}

Gotcha

A class with any abstract method must itself be abstract. abstract cannot combine with final (nothing could ever implement it), and an abstract method cannot be private, static, or final. A concrete subclass that forgets to implement an inherited abstract method does not compile.

Polymorphism: object type vs reference type

A reference's declared (reference) type decides which members are visible at compile time; the object's runtime type decides which overridden instance method actually runs (dynamic dispatch, a.k.a. virtual invocation).

java
class Animal { String speak() { return "..."; } }
class Dog extends Animal {
    @Override String speak() { return "woof"; }
    void fetch() { }
}

Animal a = new Dog();
a.speak();    // "woof"  — dispatched on the OBJECT type (Dog)
a.fetch();    // COMPILE ERROR — fetch() isn't visible through the Animal reference

This is what makes polymorphism useful: code written against the supertype transparently runs each object's own override. Hold mixed subtypes in one supertype-typed collection, or accept a supertype parameter, and dispatch still picks the right method:

java
List<Animal> zoo = List.of(new Dog(), new Animal());
for (Animal x : zoo) System.out.println(x.speak());   // "woof", then "..."

To call a subtype-only member like fetch(), narrow the reference back with a cast (see below).

Overriding vs hiding vs overloading

Overriding replaces an inherited instance method; dispatch is dynamic (object's runtime type). Hiding applies to static methods and fields; resolution is static (reference's compile-time type). Overloading (Lesson 01) is merely same name + different parameters, also resolved statically.

java
class A { String who() { return "A"; }   static String s() { return "A.s"; } }
class B extends A {
    @Override String who() { return "B"; }   // overrides — dynamic
    static String s() { return "B.s"; }       // hides — static
}

A ref = new B();
ref.who();   // "B"   — instance method, dynamic dispatch
ref.s();     // "A.s" — static method, resolved by the reference type A
OverloadingOverridingHiding
Applies tosame name, different paramsinstance methodsstatic methods & fields
Resolved bycompile-time (argument types)runtime (object type)compile-time (reference type)
@Override valid?NoYesNo

Exam trap

Fields are never polymorphic — a field access uses the reference type, not the object type. With class A{int x=1;} class B extends A{int x=2;}, ((A) new B()).x is 1.

You cannot override static, final, or private methods:

  • a final method → trying to override is a compile error;
  • a static method → a same-signature subclass method hides it (not overrides);
  • a private method isn't inherited → a same-signature subclass method is a brand-new, independent method (no polymorphism).

A valid override must also obey these rules — @Override (which works for interface methods too) makes the compiler check them:

RuleAllowedNot allowed
Accesssame or wider (protectedpublic)narrowing (publicprotected)
Return typesame or a covariant subtypean unrelated or wider type
Checked exceptionssame, fewer, or narrowernew or broader checked exceptions
java
class Producer { Object make() { return "x"; } }
class StringProducer extends Producer {
    @Override String make() { return "y"; }   // covariant return: String is-a Object
}

Object methods: equals, hashCode, toString

Every class extends Object. Its defaults are identity-based: equals is ==, hashCode is the identity hash, and toString is getClassName@hexHash. Override the trio together and consistently:

  • equals(Object) — logical equality. If you override it, override hashCode too, or hash-based collections break.
  • hashCode() — equal objects must return equal hash codes.
  • toString() — a readable representation (replaces Point@1b6d3586).
java
@Override public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Point p)) return false;   // pattern: null-safe + cast in one
    return x == p.x && y == p.y;
}
@Override public int hashCode() { return Objects.hash(x, y); }

equals must honor its contract: it is reflexive (x.equals(x)), symmetric, transitive, and consistent, and x.equals(null) is always false.

Gotcha

equals takes an Object parameter. Writing equals(Point p) overloads rather than overrides — collections still call the inherited Object.equals (identity); @Override would flag it. Using instanceof in equals accepts subclass instances (can break symmetry if a subclass adds state); getClass() enforces exact-type equality instead — pick deliberately. (Object also declares getClass, clone, and the wait/notify family — the last covered in Module 07.)

final

final locks something against change, and what it locks depends on where it sits:

  • on a class — blocks subclassing (final class String),
  • on a method — blocks overriding,
  • on a variable/field — allows exactly one assignment.
java
final int x = 1;                 // assigned at declaration
final int y;                     // "blank final"
y = 2;                           // OK once; a second assignment would not compile

final List<Integer> list = new ArrayList<>();
list.add(1);                     // OK — the OBJECT is still mutable
// list = new ArrayList<>();     // COMPILE ERROR — the REFERENCE is final

Exam trap

final on a reference freezes the reference, not the object — final List can still be mutated with add/remove; only reassignment is rejected. A blank final field must be definitely assigned by the end of every constructor, or the class won't compile. (Method parameters can be final too.)

Casting and instanceof

Upcasting to a supertype is implicit (no cast needed). Downcasting to a subtype needs an explicit cast and is checked at runtime. Whether a cast even compiles depends on the static types:

  • types in the same hierarchy → the cast compiles, and throws ClassCastException at runtime if the object isn't actually that type;
  • unrelated final types → the cast is a compile error ("inconvertible types").
java
Animal a = new Dog();       // upcast — implicit, no cast needed

Object o = Integer.valueOf(1);
String bad = (String) o;    // compiles (o is Object) → ClassCastException at runtime

String x = (String) Integer.valueOf(1);   // COMPILE ERROR — Integer & String are unrelated finals

Guard a downcast with instanceof — ideally the pattern form, which casts for you:

java
Object obj = "hi";
if (obj instanceof String s) { use(s.length()); }   // safe, no separate cast

Exam trap

null instanceof AnyType is always false — it never throws. And instanceof between unrelated final types (e.g. "x" instanceof Integer) is a compile error, mirroring the casting rule. The pattern variable's scope (flow scoping) is covered in Lesson 08.

SDET note

Overriding, construction order, and final are where "looks right, compiles wrong" AI suggestions hide — a constructor calling an overridable method, a broken equals/hashCode pair, or a cast that can't compile. Let the compiler and a unit test, not a glance, confirm the behavior.

Key Takeaways

  • A subclass extends one class and inherits its accessible, non-private members; constructors and private members are not inherited.
  • A subclass constructor runs super(...) first; init order is all statics (super→sub, once), then per object the super instance init + ctor, then the sub instance init + ctor. A super ctor calling an overridable method sees subclass fields still uninitialized.
  • An abstract class can't be instantiated; an abstract method forces concrete subclasses to override it. abstract can't pair with final/private/static.
  • Instance methods override (dynamic, by object type); static methods and fields hide (static, by reference type); overloading is by parameters. You cannot override static/final/private methods. Fields are never polymorphic.
  • Reference type controls visible members at compile time; object type controls which override runs. Overrides may widen access, narrow return type (covariant), and not broaden checked exceptions — use @Override.
  • Override equals/hashCode together; equals takes Object, is reflexive/symmetric/ transitive/consistent, and x.equals(null) is false. Defaults are identity-based.
  • final locks a class (no subclass), a method (no override), or a variable (one assignment) — a final reference still allows mutating the object.
  • Upcasts are implicit; downcasts need a cast. A same-hierarchy cast compiles and may throw ClassCastException; an unrelated-final cast is a compile error. null instanceof X is always false.

Lesson Quiz

Lesson Quiz · Inheritance & Polymorphism0 / 11
  1. What does ref.who() and ref.s() print?

    class A { String who(){return "A";} static String s(){return "A";} }
    class B extends A { String who(){return "B";} static String s(){return "B";} }
    A ref = new B();
    • AB and B
    • BB and A
    • CA and A
    • DA and B
  2. What does new Child() print?

    class Parent { { System.out.print("P-init "); } Parent(){ System.out.print("P-ctor "); } }
    class Child extends Parent { { System.out.print("C-init "); } Child(){ System.out.print("C-ctor "); } }
    • AP-init P-ctor C-init C-ctor
    • BC-init C-ctor P-init P-ctor
    • CP-ctor P-init C-ctor C-init
    • DP-init C-init P-ctor C-ctor
  3. What does new Derived() print?

    class Base { Base(){ init(); } void init(){ System.out.println("base"); } }
    class Derived extends Base { String tag = "ready"; void init(){ System.out.println(tag); } }
    • Aready
    • Bnull
    • Cbase
    • DCompile error
  4. What is ((A) new B()).x ?

    class A { int x = 1; }
    class B extends A { int x = 2; }
    • A1
    • B2
    • CCompile error
    • D0
  5. Which can you NOT override in a subclass?

    • AA public instance method
    • BA protected instance method
    • CA final method
    • DA method returning a subtype (covariant)
  6. Which statement about an abstract class is TRUE?

    • AIt can be instantiated with new
    • BIt cannot be instantiated, but can have constructors and concrete methods
    • CAll its methods must be abstract
    • DIt can be both abstract and final
  7. Why does this subclass not compile?

    class Animal { Animal(String n) {} }
    class Cat extends Animal { Cat() {} }
    • ACat needs @Override
    • BAnimal has no no-arg constructor, so implicit super() fails
    • CCat must be abstract
    • DIt compiles fine
  8. Given final List<Integer> l = new ArrayList<>(); which line fails to compile?

    • Al.add(1);
    • Bl.remove(0);
    • Cl = new ArrayList<>();
    • Dl.clear();
  9. What is the result?

    Object o = Integer.valueOf(1);
    String s = (String) o;
    • ACompiles, throws ClassCastException at runtime
    • BCompile error — inconvertible types
    • CPrints 1
    • DReturns null
  10. What is the value of (null instanceof String) ?

    • Atrue
    • Bfalse
    • CNullPointerException
    • DCompile error
  11. Which signature actually OVERRIDES Object.equals?

    • Aboolean equals(Point p)
    • Bboolean equals(Object o)
    • Cint equals(Object o)
    • Dboolean Equals(Object o)

Next: Interfaces. Run the matching code in labs/src/main/java/com/jse21/m03_oop/.