Appearance
Lesson 02 · Inheritance & Polymorphism
Objectives
After this lesson you will be able to:
- Use
extendsandsuper, and know what a subclass inherits. - Predict the full initialization order (statics and instances) across a hierarchy.
- Declare and use
abstractclasses and methods. - Tell overriding from hiding and overloading, and predict polymorphic dispatch.
- Differentiate object type from reference type.
- Override
Objectmethods (equals,hashCode,toString) under their contracts. - Use
final(class, method, and variable), casting, andinstanceofsafely.
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:
| Member | Inherited? |
|---|---|
public / protected methods & fields | Yes |
| package-private members | Yes only if the subclass is in the same package |
private members | No — they exist in the object but aren't accessible by name |
static members | Yes (but they belong to the class, and are hidden not overridden) |
| Constructors | No — 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:
- Static fields and static blocks of each class, superclass before subclass, in source order — once, when each class is first loaded.
- For each
new: the superclass instance field initializers and instance blocks, then the superclass constructor body. - 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-ctorThe 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 assignedExam 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 referenceThis 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| Overloading | Overriding | Hiding | |
|---|---|---|---|
| Applies to | same name, different params | instance methods | static methods & fields |
| Resolved by | compile-time (argument types) | runtime (object type) | compile-time (reference type) |
@Override valid? | No | Yes | No |
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
finalmethod → trying to override is a compile error; - a
staticmethod → a same-signature subclass method hides it (not overrides); - a
privatemethod 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:
| Rule | Allowed | Not allowed |
|---|---|---|
| Access | same or wider (protected → public) | narrowing (public → protected) |
| Return type | same or a covariant subtype | an unrelated or wider type |
| Checked exceptions | same, fewer, or narrower | new 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, overridehashCodetoo, or hash-based collections break.hashCode()— equal objects must return equal hash codes.toString()— a readable representation (replacesPoint@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 finalExam 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
ClassCastExceptionat runtime if the object isn't actually that type; - unrelated
finaltypes → 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 finalsGuard 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 castExam 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
extendsone class and inherits its accessible, non-private members; constructors andprivatemembers 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
abstractclass can't be instantiated; anabstractmethod forces concrete subclasses to override it.abstractcan't pair withfinal/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/privatemethods. 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/hashCodetogether;equalstakesObject, is reflexive/symmetric/ transitive/consistent, andx.equals(null)isfalse. Defaults are identity-based. finallocks a class (no subclass), a method (no override), or a variable (one assignment) — afinalreference still allows mutating the object.- Upcasts are implicit; downcasts need a cast. A same-hierarchy cast compiles and may throw
ClassCastException; an unrelated-finalcast is a compile error.null instanceof Xis alwaysfalse.
Lesson Quiz
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();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 "); } }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); } }What is ((A) new B()).x ?
class A { int x = 1; } class B extends A { int x = 2; }Which can you NOT override in a subclass?
Which statement about an abstract class is TRUE?
Why does this subclass not compile?
class Animal { Animal(String n) {} } class Cat extends Animal { Cat() {} }Given final List<Integer> l = new ArrayList<>(); which line fails to compile?
What is the result?
Object o = Integer.valueOf(1); String s = (String) o;
What is the value of (null instanceof String) ?
Which signature actually OVERRIDES Object.equals?
Next: Interfaces. Run the matching code in labs/src/main/java/com/jse21/m03_oop/.