Robust Oop
Inheritance & Polymorphism
A subclass extends a superclass to inherit and specialize it. Its constructor runs the superclass constructor first, and private members stay behind. Overriding replaces a method, and super reaches the parent's version. The payoff is dynamic dispatch: calling an overridden method through a superclass reference runs the version for the object's actual run-time type, not the declared type. Fields and static methods don't dispatch, a downcast is checked at run time and can throw ClassCastException, final stops overriding, and every object inherits Object's methods. Shown with verified output.
Suggest an editInheritance & Polymorphism — One Reference, Many Behaviors
Classes let you model one type of thing. Inheritance lets one class build on top of another:
- A subclass
extendsa superclass. It inherits the superclass's fields and methods, and adds its own. - It can override an inherited method — that is, redeclare it, so its own version replaces the parent's.
- The real prize is polymorphism. When you call an overridden method through a superclass reference, Java runs the version that belongs to the object's actual runtime type.
Picture a loop over an Animal[] that holds a Dog and a Cat: each one speaks in its own voice. The mechanism behind this is called dynamic dispatch. It lets code written against a general type drive specialized behavior it has never even seen.
This lesson also shows you where dispatch stops: at fields and static methods, at casts back to the specific type, at final, and at the methods that every object inherits from Object.
💡 The core idea.
- A subclass
extendsa superclass, inheriting and overriding its members. - The prize is polymorphism: an overridden call through a superclass reference runs the version for the object's runtime type.
- The mechanism is dynamic dispatch — general code drives specialized behavior.
- It has limits: fields don't dispatch,
finalshuts it off, and every object inherits fromObject.
This lesson takes a deeper pass at classes & objects. Every output below was produced by compiling and running the code on Java 25.
You'll be able to: trace the order in which a subclass's constructors run, and fix a missing super(...) call; predict which version runs when you reach an overridden method, a hidden field or a static method through a superclass reference; predict whether a reference cast compiles, succeeds or throws ClassCastException, and guard it with instanceof; name the compiler error for a misspelled override, a narrowed access level, and an override of a final method; choose between inheritance and composition using the "is-a" test.
📘 How to read the Intuition boxes. Each one is built in three moves:
- The mechanism — what the compiler and the JVM do.
- A concrete bite — a specific, runnable failure (often a real compiler error), shown so the trap is visible.
- The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.
Table of contents
extends: inheriting and adding- Overriding and
super - Dynamic dispatch: polymorphism
- Reference casting:
instanceofandClassCastException finaland theObjectmethods- Mental-model summary
- Gotcha checklist
- Check yourself
- Sources
1. extends: inheriting and adding
A subclass declares extends Superclass, and it automatically gets the superclass's fields and methods. Its constructor calls the superclass constructor with super(...). On top of that, the subclass can add members of its own.
Output:
Rex is an animal
Rex fetches the ballAnalysis.
Doginheritednameanddescribe()fromAnimal, sod.describe()worked even thoughDognever defines it.Dogadded its ownfetch().Dog's constructor calledsuper(name)to runAnimal's constructor, which setsname.- The arrow in the diagram (
<|--) reads "Dog is an Animal."
The superclass part of the object is set up before the subclass part. Before JDK 25, the language enforced this by making super(...) the first statement in the constructor. Since JDK 25, statements may come before super(...), as long as they don't use the object that's being built [3] [15]. Later in this section, you'll see why that matters.
Intuition.
Mechanism. new Dog("Rex") creates one object, with room for both the fields declared in Animal and those declared in Dog [17]. Construction runs top-down: the Animal constructor initializes the inherited fields first, and then the Dog constructor finishes the job.
What if you don't write super(...) at all? Then javac inserts super(); — a call to the superclass's no-argument constructor — as the first statement [3]. You can see the order when each constructor prints something:
Output:
1. Animal's constructor
2. Dog's constructorDog() never mentions Animal, yet Animal's constructor ran first, because the inserted super(); called it.
Concrete bite. That inserted call only works if the superclass has a no-argument constructor. Animal below has only Animal(String name), so the implicit super(); has nothing to call:
Compiler error:
Main.java:7: error: constructor Animal in class Animal cannot be applied to given types;
Dog(String name) { }
^
required: String
found: no arguments
reason: actual and formal argument lists differ in lengthThe caret points at Dog's body, which is where the invisible super(); sits. The fix is the super(name); from the first example. Note that constructors aren't inherited either: Dog gets no Dog(String) unless it declares one itself [2].
Non-example: a superclass constructor calls an overridden method. Animal's constructor calls describe(), and Dog overrides it. The call runs Dog's version, before Dog's constructor has set name:
Output:
a dog named nullThe field was still null, its default value, even though it's final and the constructor sets it. Construction runs top-down, so Animal() ran, and called describe(), before the line this.name = name;.
Since JDK 25, a constructor may assign its own fields before it calls super(...) [15]. Move the assignment up, and the field is ready by the time the superclass needs it:
Output:
a dog named RexThe code before super(...) can assign fields, check arguments and compute values. It can't read a field or call an instance method of the object, because the object isn't ready yet. javac rejects that with cannot reference … before supertype constructor has been called.
💡 Earned rule. Don't call an overridable method from a constructor: the subclass version runs before the subclass is set up. If you can't avoid it, assign the fields that method needs before super(...) (JDK 25+).
Non-example: a private field is not inherited. Members declared private are not inherited by subclasses [2]. The field still exists inside a Dog object, but Dog's code can't refer to it by name:
Compiler error:
Main.java:6: error: secret has private access in Animal
String reveal() { return secret; }
^
1 errorTo fix it, give the superclass a method that returns the value, or mark the field protected so subclasses can see it (access levels).
Non-example: inheriting only to reuse code. The test for whether to inherit is the "is-a" relationship. A Dog genuinely is an Animal. But the JDK's own java.util.Stack fails this test: it extends Vector, which is a growable list [14]. As a result, every list method comes along for the ride — including insert-at-any-position, which breaks "last in, first out":
Output:
[sneaked in, first, second]
second
sneaked inadd(0, …) slipped an element under the stack, and get(0) read the bottom. A real stack should allow neither. In fact, Stack's own API page recommends using the Deque interface instead [14]. A stack has a list; it isn't one. Holding the list in a private field is called composition, and it exposes only the operations you choose to write.
💡 Earned rule. Use extends only for a true "is-a" specialization, and call super(...) to initialize the inherited state.
- The cost of inheritance is tight coupling: the subclass depends on the superclass's internals and inherits its entire API.
- The payoff is genuine specialization, plus the polymorphism that comes with it.
- So use inheritance for "is-a", and favor composition ("has-a") everywhere else.
2. Overriding and super
A subclass overrides an inherited method by redeclaring it with the same signature. For objects of that subclass, its version replaces the superclass's. When you need both, super.method() still reaches the superclass's version.
Output:
Woof
... / WoofAnalysis.
Dog.speak()overrodeAnimal.speak(), sod.speak()is"Woof".- Inside
both(),super.speak()calledAnimal's version ("..."), while a plainspeak()calledDog's ("Woof"). Together, they give"... / Woof".
super is your escape hatch back to the parent's behavior. You'll often use it to extend that behavior: "do what the parent does, plus this."
Intuition.
Mechanism. An override must have the same signature as the inherited method: the same name and the same parameter types [4]. The @Override annotation asks the compiler to verify that for you. If the method doesn't actually override anything, that's an error [5] — which catches typos and signature mismatches.
Concrete bite. Misspell the method name (or get the parameters wrong), and you've written a new method, not an override. @Override turns that silent mistake into a compile error:
Compiler error:
Main.java:4: error: method does not override or implement a method from a supertype
@Override
^speakk (note the typo) overrides nothing, so @Override flags it. Without the annotation, this would compile as a brand-new method that nobody ever calls, and speak() would quietly keep the parent's behavior.
Non-example: an override that narrows access. An override must give at least as much access as the method it replaces [6]. Object.toString() is public. If you leave public off, the name and parameters still match, but the override is rejected:
Compiler error:
Main.java:3: error: toString() in Animal cannot override toString() in Object
String toString() { return "an animal"; }
^
attempting to assign weaker access privileges; was public
1 errorThe fix is to declare it public String toString(), as §5 does. The return type, by the way, has one degree of freedom: an override may return a subtype of the original's return type [6].
💡 Earned rule. Always annotate overrides with @Override, and use super.method() when you need to build on the parent's behavior. The cost is a single annotation. The payoff: a misspelled or mis-signed "override" that would otherwise silently do nothing becomes a compile error you can fix right away.
3. Dynamic dispatch: polymorphism
Now for the payoff. A variable's declared type can be a superclass, while the object it points to is a subclass.
- Assigning a
Dogto anAnimalvariable is called an upcast. It needs no cast syntax, because everyDogis anAnimal. - When you call an overridden method, Java runs the version for the object's actual runtime type — not the declared type. This is dynamic dispatch.
Output:
Rex: Woof
Felix: Meow
Thing: ...Analysis. Every element is declared as an Animal, yet each a.speak() ran the object's own version: Dog's Woof, Cat's Meow, Animal's .... The loop is written against Animal and knows nothing about Dog or Cat, yet it drives their specialized behavior. That's polymorphism: one interface (speak() on Animal), many implementations, with the actual type choosing between them at run time.
Intuition. Mechanism. Two types are in play at every call, and each one decides one thing:
- The declared type decides, at compile time, which methods you may call at all.
- For an instance method, the object's class decides, at run time, which override runs [7].
That's why the loop's Animal reference can invoke Dog.speak(). But the first rule has a bite of its own: Dog's extra method is out of reach through an Animal variable, even when the object really is a Dog:
Compiler error:
Main.java:14: error: cannot find symbol
System.out.println(a.fetch());
^
symbol: method fetch()
location: variable a of type Animal
1 errorThe compiler looked for fetch() in Animal, the declared type, and didn't find it. §4 shows you how to reach it.
Concrete bite. The dynamic rule applies to methods, not fields. A field access is resolved by the declared type, at compile time:
Output:
Woof
animala.speak() dispatched dynamically to Dog ("Woof"), but a.kind read Animal's field ("animal"). Java uses only the declared type to pick the field, never the object's class [8].
Dog's kind hides Animal's, and both fields exist side by side in the object. Hiding a field like this is a code smell precisely because it causes this kind of confusion. Override methods; don't hide fields.
Non-example: a static method does not dispatch. A static method belongs to the class, not to an object. So a subclass's static method with the same signature hides the parent's, rather than overriding it [9]. Just like a field, it's picked by the declared type:
Output:
Woof
animala.kind() compiled, but it ran Animal.kind(). Call a static method through its class name instead (Dog.kind()), and the code says exactly what it does.
💡 Earned rule. Program against the general (super)type, and let overridden methods dispatch to the right behavior. That's how polymorphic code stays open to new subclasses without having to change.
- The cost is an asymmetry: fields and
staticmethods bind to the declared type, so never rely on "overriding" them. - The payoff is code that works with subclasses written long after it was — as long as they override the methods it calls.
4. Reference casting: instanceof and ClassCastException
You met casts on numbers in Numbers & Arithmetic, and instanceof paired with a cast in equals & hashCode. On references, a cast changes only the declared type. It never changes the object itself.
- An upcast (
DogtoAnimal) is always safe, so it happens automatically. - A downcast (
AnimaltoDog) needs the cast syntax,(Dog) a. It's a promise that the object is aDog, and the JVM checks that promise at run time [10]. a instanceof Dogasks the same question without the risk. It'struewhen the object could be cast toDog, andfalsefornull[12].
Guard the downcast with instanceof, and fetch() is back within reach:
Output:
Woof, then fetches the ball
Meow, and no fetchAnalysis. The Dog passed the instanceof test, so the cast was safe, and d.fetch() compiled because d is declared as a Dog. The Cat failed the test, so the code never attempted the cast. Meanwhile, a.speak() dispatched as usual; the cast was needed only for fetch().
Intuition.
Mechanism. A downcast gets checked twice. First, the compiler rejects any cast between two types that no object could belong to at once. Then the JVM checks the object's class at the moment of the cast. If the object isn't a Dog (or a subclass of Dog), the cast throws a ClassCastException [11].
Concrete bite. This cast compiles, because an Animal could be a Dog. But this particular one is a Cat:
Output (prints about to cast, then a thrown exception):
about to cast
Exception in thread "main" java.lang.ClassCastException: class Cat cannot be cast to class Dog (Cat and Dog are in unnamed module of loader 'app')The message names both classes: the object's actual class (Cat) and the cast's target (Dog). The failure happens at run time, on line 9 — far from the line that put a Cat into a.
Non-example: a cast between unrelated types. A Dog can never be a String. Both are classes, and neither one extends the other, so the compiler refuses the cast outright:
Compiler error:
Main.java:7: error: incompatible types: Dog cannot be converted to String
String s = (String) d;
^
1 errorA cast can't turn an object into a different class of object. If you want text from a Dog, call a method that builds it, such as toString().
💡 Earned rule. Before a downcast, test with instanceof — or know the object's class for certain. Better still, avoid the downcast entirely: move the behavior into an overridden method, and let dispatch pick it.
- The cost of a downcast is a run-time check that can fail far away from the bug that caused it.
instanceofturns that failure into an ordinaryif.- Sealed Classes & Pattern Matching fuses the test and the cast into one step:
a instanceof Dog d.
5. final and the Object methods
Putting final on a method forbids overriding it, so no subclass can change that behavior. Meanwhile, every class other than Object has a superclass — and if you don't write extends, that superclass is Object itself [1]. So every object already comes with methods like toString, equals, and hashCode, which you override to give them meaning.
Output:
Animal(Rex)
unknownAnalysis.
System.out.println(a)called the overriddentoString(), givingAnimal(Rex).Object's default would have printed the class name, an@, and the hash code in hexadecimal [13] — something likeAnimal@1b6d3586.species()isfinal: you can use it, but it's locked against overriding.
Overriding toString/equals/hashCode is how your types get readable printing and correct value equality. You inherit these methods from Object whether you ask for them or not.
Intuition.
Mechanism. Object is the root of every class hierarchy, so its methods are always there to override. A final method, on the other hand, can't be overridden or hidden: the compiler rejects any attempt [16]. Its behavior is fixed for the whole hierarchy.
Concrete bite. Try to override a final method, and it won't compile:
Compiler error:
Main.java:2: error: species() in Dog cannot override species() in Animal
class Dog extends Animal { @Override String species() { return "dog"; } }
^Animal.species() is final, so Dog can't override it — the lock holds. javac's next line spells out the reason: overridden method is final.
Non-example: extending a final class. Putting final on a class forbids subclasses altogether [1]. String is one such final class:
Compiler error:
Main.java:1: error: cannot inherit from final String
class Shout extends String { }
^
1 errorTo add behavior to text, write a class that has a String field (composition), or write a static helper method.
💡 Earned rule. Override Object's toString — plus equals/hashCode for value types — to give your objects meaning. Mark a method final when subclasses must not change its behavior, such as for a security or invariant guarantee.
- The cost of
finalis lost flexibility: no subclass can specialize that method. - The payoff is behavior you can rely on across the whole hierarchy.
6. Mental-model summary
| Principle | Consequence |
|---|---|
A subclass extends a superclass and super(...)-initializes it |
It inherits fields/methods; use inheritance only for a true "is-a" |
With no super(...), javac inserts super(); |
The superclass constructor runs first; with no no-argument constructor, it does not compile |
Statements before super(...) may set fields but not use the object (JDK 25) |
Assign the fields an overridden method needs before super(...), or the superclass sees their defaults |
private members and constructors are not inherited |
A subclass reaches a private field through a method or a protected field |
Overriding redeclares an inherited method; super.m() reaches the parent |
Annotate with @Override so a typo'd "override" is a compile error; never narrow the access |
| The declared type decides which methods compile; the object's class decides which override runs | a.fetch() does not compile on an Animal variable, even when the object is a Dog |
Fields and static methods bind to the declared type |
a.kind and a.kind() use Animal's — they hide, they don't override |
| A downcast is checked at run time | A wrong one throws ClassCastException; test with instanceof first |
final forbids overriding (on a method) and subclassing (on a class); every class extends Object |
Override toString/equals for meaning |
7. Gotcha checklist
| Symptom | Likely cause | Fix |
|---|---|---|
constructor Animal in class Animal cannot be applied to given types, at a subclass constructor |
no super(...), so javac inserted super();, and the superclass has no no-argument constructor |
call super(...) with the arguments, as the first statement |
secret has private access in Animal, in subclass code |
private members are not inherited |
add a method to the superclass, or make the field protected |
| An "override" is ignored; the parent method still runs | a typo or different parameter types made it a new method | add @Override, and the compiler catches it |
method does not override or implement a method from a supertype |
@Override on a method that overrides nothing |
fix the name and the parameter types |
attempting to assign weaker access privileges; was public |
the override has narrower access, such as toString() without public |
give it the same access or wider |
cannot find symbol … location: variable a of type Animal |
the method exists only in the subclass; the declared type decides what compiles | test instanceof, then downcast; or declare the method in the superclass |
ClassCastException: class Cat cannot be cast to class Dog |
a downcast to a class the object is not | test with instanceof before the cast |
incompatible types: Dog cannot be converted to String, on a cast |
the types are unrelated, so no object could be both | call a method that converts, such as toString() |
| A field read through a superclass variable gives the parent's value | fields are not polymorphic; the subclass field hides the parent's | override methods; don't redeclare fields |
A static method call runs the superclass version |
static methods hide; they do not override |
call it through the class name |
cannot override … then overridden method is final |
the superclass method is final |
leave it; the design forbids the change |
cannot inherit from final String |
the class is final |
use composition: a field of that type |
println(obj) prints ClassName@1b6d3586 |
the default Object.toString |
override public String toString() |
The class exposes methods that break its rules (Stack.add(0, …)) |
"is-a" was not true; it inherited a whole list API | prefer composition: a private field |
✅ Check yourself
One check per objective. Answer before you open anything.
The 🧪 box below: Bird, the hidden field, and overriding a final method.
Output:
Woof
Meow
Tweet
Woof / animal- Each
speak()ran the object's own override: that's dynamic dispatch. a.speak()isWoof, buta.kindisanimal, because the field is picked by the declared type,Animal.- Overriding a
finalmethod doesn't compile: you getspecies() in Dog cannot override species() in Animal, followed byoverridden method is final. Methods are chosen by the object's class at run time; fields are chosen by the declared type at compile time.
📚 Sources
- The Java Language Specification, Java SE 25, §8.1.4 "Superclasses and Subclasses" (the default direct superclass is
Object; afinalclass may not be named inextends) — https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.1.4 - The Java Language Specification, Java SE 25, §8.2 "Class Members" ("Members of a class that are declared private are not inherited"; constructors are not members) — https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.2
- The Java Language Specification, Java SE 25, §8.8.7 "Constructor Body" (the implicit
super();; the prologue and the early construction context) — https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.8.7 - The Java Language Specification, Java SE 25, §8.4.2 "Method Signature" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.4.2
- The Java Language Specification, Java SE 25, §9.6.4.4 "
@Override" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-9.html#jls-9.6.4.4 - The Java Language Specification, Java SE 25, §8.4.8.3 "Requirements in Overriding and Hiding" (return-type-substitutable; "at least as much access") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.4.8.3
- The Java Language Specification, Java SE 25, §15.12.4.4 "Locate Method to Invoke" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.12.4.4
- The Java Language Specification, Java SE 25, §15.11.1 "Field Access Using a Primary" ("only the type of the Primary expression … is used in determining which field to use") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.11.1
- The Java Language Specification, Java SE 25, §8.4.8.2 "Hiding (by Class Methods)" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.4.8.2
- The Java Language Specification, Java SE 25, §5.1.6 "Narrowing Reference Conversion" (may require a test at run time) — https://docs.oracle.com/javase/specs/jls/se25/html/jls-5.html#jls-5.1.6
- The Java Language Specification, Java SE 25, §15.16 "Cast Expressions" (
ClassCastException"if a cast is found to be impermissible at run time") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.16 - The Java Language Specification, Java SE 25, §15.20.2 "The
instanceofOperator" (the null reference givesfalse) — https://docs.oracle.com/javase/specs/jls/se25/html/jls-15.html#jls-15.20.2 java.lang.Object, Java SE 25 API (toString:getClass().getName() + '@' + Integer.toHexString(hashCode())) — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Object.htmljava.util.Stack, Java SE 25 API (extends Vector;Deque"should be used in preference to this class") — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Stack.html- JEP 513: Flexible Constructor Bodies (JDK 25) — https://openjdk.org/jeps/513
- The Java Language Specification, Java SE 25, §8.4.3.3 "
finalMethods" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.4.3.3 - The Java Language Specification, Java SE 25, §12.5 "Creation of New Class Instances" (room for "all the instance variables that may be hidden") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-12.html#jls-12.5
🧪 Predict, then check.
- Add a
Bird extends Animalthat overridesspeak()to return"Tweet". Put aDog, aCat, and aBirdin anAnimal[], then predict the three lines printed when you callspeak()on each. - Predict what
a.speak()anda.kindprint forAnimal a = new Dog();, given the §3 hidden field. - Predict the compiler's reaction to a subclass overriding a
finalmethod, and explain why fields and methods resolve differently.
Your Turn
Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.