Inheritance and polymorphism
extends and super, overriding against overloading, dynamic dispatch, upcasting, final, and why multiple class inheritance is out.
Inheritance is the mechanism. Polymorphism is the payoff. Most candidates can define both and cannot demonstrate the difference, so this lesson does it with code you could reproduce on a whiteboard.
extends
class Person {
protected String name;
Person(String name) {
this.name = name;
}
String greet() {
return "I am " + name;
}
}
class Student extends Person {
private String roll;
Student(String name, String roll) {
super(name); // MUST be the first statement
this.roll = roll;
}
@Override
String greet() { // overriding
return super.greet() + ", roll " + roll;
}
}System.out.println(new Student("Raja", "05H4").greet());
// I am Raja, roll 05H4super(name) calls the parent constructor. If you omit it, the compiler inserts super() — and
if the parent has no no-argument constructor, that is a compile error. This is the single most
common inheritance mistake.
super.greet() calls the parent's version of an overridden method, which is how you extend
behaviour rather than replace it.
protected means visible to subclasses and to the same package. It is the access level that exists
specifically for inheritance.
Overriding versus overloading
These two words look similar and mean unrelated things. Getting the distinction crisp is worth real marks.
| Overriding | Overloading | |
|---|---|---|
| Where | subclass redefines a parent method | same class, several methods share a name |
| Signature | must be identical | must differ |
| Return type | same, or a subtype (covariant) | may be anything |
| Resolved | at run time, by the object | at compile time, by the arguments |
| Called | dynamic dispatch / late binding | static binding |
| Access | cannot be more restrictive | unrestricted |
Applies to static |
no — that is hiding | yes |
class Parent {
void show() { System.out.println("Parent"); }
void show(int x) { System.out.println("Parent " + x); } // overload
}
class Child extends Parent {
@Override
void show() { System.out.println("Child"); } // override
}Always write @Override. It is not decoration — it makes the compiler check that you really are
overriding something. Misspell the method name, or get a parameter type wrong, and without the
annotation you have silently added a new method while the parent's version keeps being called. That
bug can survive a long time.
The rules for a valid override
- Same name, same parameter types, in that order.
- Return type identical, or a subtype of the parent's (covariant return).
- Access may widen (
protected→public) but never narrow (public→protectedis an error). - It may throw fewer or narrower checked exceptions, never broader ones. Unchecked exceptions are unrestricted.
private,staticandfinalmethods cannot be overridden.
Polymorphism, demonstrated
A parent-typed reference can hold a child object, and calling a method on it runs the child's version:
class Round {
String marking() { return "not defined"; }
}
class Aptitude extends Round {
@Override String marking() { return "1 mark, minus 0.25 negative"; }
}
class Coding extends Round {
@Override String marking() { return "hidden test cases, partial credit"; }
}
class Interview extends Round {
@Override String marking() { return "content, structure and delivery"; }
}List<Round> drive = List.of(new Aptitude(), new Coding(), new Interview());
for (Round r : drive) {
System.out.println(r.marking()); // each prints its own
}
// 1 mark, minus 0.25 negative
// hidden test cases, partial credit
// content, structure and deliveryThe loop variable's type is Round. The method that runs is decided by the object, at run
time. That is dynamic dispatch, and it is the whole point: adding a fourth round type requires
no change to this loop.
The reference type still controls what you are allowed to call:
Round r = new Coding();
r.marking(); // fine — declared in Round
r.runTests(); // compile error, even though Coding has it
((Coding) r).runTests(); // downcast, now allowedUpcasting (child → parent) is always safe and implicit. Downcasting (parent → child) is
explicit and can fail at run time with ClassCastException, so guard it:
if (r instanceof Coding c) { // Java 16+: tests and binds in one step
c.runTests();
}Fields are not polymorphic
A trap that appears in "what does this print" questions:
class Parent { String label = "parent"; }
class Child extends Parent { String label = "child"; }
Parent p = new Child();
System.out.println(p.label); // "parent" ← the REFERENCE type decides
System.out.println(((Child) p).label); // "child"Methods are dispatched on the object; fields are resolved on the reference type, at compile time. This is exactly why you keep fields private and expose them through methods.
Compile-time versus run-time polymorphism
Interviewers often ask for "the two types of polymorphism":
- Compile-time (static polymorphism) — method overloading. The compiler picks by argument types.
- Run-time (dynamic polymorphism) — method overriding. The JVM picks by the object's actual class.
Only the second is what most people mean by polymorphism, and only the second gives you the loop above.
final
Three uses, three different meanings:
final int MAX = 100; // variable: cannot be reassigned
final void audit() { } // method: cannot be overridden
final class Sealed { } // class: cannot be extendedString, Integer and the other wrappers are final classes — you cannot subclass them, and
their immutability therefore cannot be subverted.
An important subtlety: final on a reference stops reassignment, not mutation:
final List<String> names = new ArrayList<>();
names.add("Raja"); // allowed — the object changed
names = new ArrayList<>(); // compile error — the reference cannotWhy no multiple class inheritance
Java allows one superclass and any number of interfaces. The reason is the diamond problem:
A
/ \
B C both override doWork()
\ /
D which doWork() does D inherit?C++ allows this and resolves it with rules that are famously hard to reason about. Java's designers removed the ambiguity by removing the feature, and gave you interfaces instead — where multiple inheritance of type is allowed because there was no implementation to conflict.
Since Java 8, interfaces can have default methods, which reintroduces the possibility of a
conflict — so the language added one rule: if two interfaces provide the same default method, the
implementing class must override it and choose. Lesson 7 covers this.
Object, the universal parent
Every class implicitly extends Object, so every object has these:
toString() // "ClassName@hashcode" unless you override it
equals(Object) // reference equality unless you override it
hashCode() // identity-based unless you override it
getClass() // the runtime class
clone() // protected; shallow copy
finalize() // deprecated, never use
wait() notify() notifyAll() // thread coordinationThe three you should override deliberately are toString, equals and hashCode — lesson 11.
What to take into an interview
extendsfor one class;super(...)must be the first statement in a subclass constructor.- Overriding is run time and by the object; overloading is compile time and by the arguments.
- Always write
@Overrideso the compiler checks you. - An override may widen access and narrow checked exceptions, never the reverse.
- Upcasting is implicit and safe; downcasting needs a cast and can throw
ClassCastException. - Methods are polymorphic; fields are not — they resolve on the reference type.
finalblocks reassignment, overriding or extension depending on where it sits.- No multiple class inheritance, because of the diamond problem; interfaces solve it.
- Every class extends
Object.