3.3 Inheritance, Method Overriding, Polymorphism, and `super`/`this`
Key Takeaways
Java enforces single class inheritance via extends; all classes ultimately inherit from java.lang.Object.
Method overriding requires matching method signatures, covariant return types, same or broader accessibility, and compatible (equal, narrower, or fewer) checked exceptions.
Static methods are not polymorphic; a subclass declaring a static method matching a superclass static method hides the superclass method, resolved by compile-time reference type.
Fields are never polymorphic in Java; variable access is bound at compile time based on the declared reference type, whereas instance method execution is dynamically dispatched at runtime.
Casting references checks class hierarchy compatibility at compile time; runtime type incompatibility throws ClassCastException.
Inheritance, Method Overriding, and Polymorphism
Inheritance and polymorphism are the structural pillars of object-oriented design in Java. Polymorphism allows an object to take many forms, enabling developers to write extensible code that operates on generalized supertypes while executing specialized subclass behavior at runtime.
1. Single Inheritance and the Object Hierarchy
In Java, a class can extend at most one direct superclass using the extends keyword. Java does not support multiple class inheritance of state or implementation. If a class declaration does not contain an explicit extends clause, the Java compiler automatically inserts extends java.lang.Object.
public class Asset {} // Implicitly: public class Asset extends java.lang.Object
public class RealEstate extends Asset {} // Directly extends Asset, indirectly extends Object
The final Class Barrier
Declaring a class with the final modifier prevents any subclassing. Attempting to extend a final class generates a compile-time error: cannot inherit from final ClassName.
2. Method Overriding Rules (The Five Core Constraints)
Method overriding enables a subclass to provide a specific implementation of an instance method already declared in its superclass. An overriding method must satisfy five strict compiler rules:
Rule 1: Exact Method Signature
The method in the subclass must have the exact same name and identical formal parameter types in the exact same order as the method in the superclass.
Rule 2: Covariant Return Types
The return type of the overriding method must be identical to, or a subtype (covariant) of, the return type declared in the superclass method.
- Reference Types: If the superclass returns
CharSequence, the subclass may returnCharSequence,String, orStringBuilder. - Primitive Types: Covariance does not apply to primitives. If the superclass returns
int, the subclass must returnint(returninglongorIntegeris a compile-time error).
Rule 3: Access Modifiers Must Not Be More Restrictive
The overriding method can maintain the same access level or provide broader access, but it cannot restrict access (public > protected > package-private > private).
- A
protectedmethod can be overridden asprotectedorpublic. - A package-private method can be overridden as package-private,
protected, orpublic. - A
publicmethod can only be overridden aspublic.
Rule 4: Exception Handling (Checked Exceptions)
The overriding method cannot declare new or broader checked exceptions than those declared in the superclass method throws clause.
- The subclass method may declare fewer checked exceptions.
- The subclass method may declare subclasses (narrower) of the declared checked exceptions.
- The subclass method may omit the
throwsclause entirely. - Unchecked Exceptions:
RuntimeExceptionandErrorare unconstrained; an overriding method can declare any unchecked exception regardless of the superclass method signature.
Rule 5: Cannot Override static or final Methods
- A method declared
finalin the superclass cannot be overridden. privatemethods are not inherited by subclasses. Defining a method with the same signature in a subclass is considered a brand new method declaration, not an override (placing@Overrideon it triggers a compile error).
class Vehicle {
protected Number getSpeed() throws java.io.IOException {
return 50;
}
}
class SportsCar extends Vehicle {
@Override
public Integer getSpeed() throws java.io.FileNotFoundException { // Valid override
return 150;
}
}
3. Method Hiding vs. Method Overriding
Static methods belong to the class, not instance objects, and therefore cannot be overridden. When a subclass declares a static method with the same signature as a superclass static method, it is called method hiding.
| Feature | Method Overriding | Method Hiding |
|---|---|---|
| Method Type | Instance methods (non-static) | Class methods (static) |
| Dispatch Mechanism | Dynamic (runtime actual object type) | Static (compile-time declared reference type) |
| Polymorphic | Yes | No |
| Annotation | Supports @Override | Cannot use @Override |
Warning
A static method cannot hide an instance method, and an instance method cannot override a static method. If one is declared static and the other is not, the compiler flags an error: static method cannot hide instance method or instance method cannot override static method.
4. Field Hiding (Variables Are Not Polymorphic)
In Java, fields are never polymorphic. If a subclass declares a field with the exact same name as a field in its superclass, the subclass field hides the superclass field.
Which field is accessed depends entirely on the declared type of the reference variable, evaluated at compile time, rather than the runtime object on the heap.
class Parent {
String name = "ParentField";
static void log() { System.out.print("ParentStatic "); }
void print() { System.out.print("ParentInstance "); }
}
class Child extends Parent {
String name = "ChildField";
static void log() { System.out.print("ChildStatic "); }
void print() { System.out.print("ChildInstance "); }
}
public class DispatchDemo {
public static void main(String[] args) {
Parent ref = new Child();
System.out.print(ref.name + " | "); // Evaluates to "ParentField" (Reference type determines field!)
ref.log(); // Evaluates to "ParentStatic " (Static method resolved by ref!)
ref.print(); // Evaluates to "ChildInstance " (Dynamic method dispatch!)
}
}
Output of the above code: ParentField | ParentStatic ChildInstance .
5. Polymorphism and Dynamic Method Dispatch
Dynamic method dispatch is the mechanism by which a call to an overridden instance method is resolved at runtime rather than compile time.
When ref.print() is executed:
- At compile time, the compiler verifies that the declared reference type (
Parent) contains an accessible method namedprint()with matching arguments. - At runtime, the JVM checks the actual object on the heap (
new Child()), inspects its virtual method table (vtable), and executesChild.print().
Using super and this
this.method(): Executes the method using normal dynamic dispatch, beginning the resolution check at the current runtime class.super.method(): Explicitly instructs the JVM to bypass the current class's implementation and invoke the immediate superclass's version of the method.
6. Reference Casting Mechanics and ClassCastException
Reference casting changes the compile-time type of a reference, altering what members are visible to the compiler.
Upcasting (Implicit & Always Safe)
Upcasting moves up the inheritance hierarchy (from subclass to superclass or interface). Because a Child is-a Parent, upcasting is always type-safe and performed automatically by the compiler:
Child c = new Child();
Parent p = c; // Implicit upcasting
Downcasting (Explicit & Potential Runtime Risk)
Downcasting moves down the inheritance hierarchy (from superclass to subclass). Because a Parent reference might not actually hold a Child instance on the heap, downcasting requires an explicit cast operator:
Parent p = new Child();
Child c = (Child) p; // Explicit downcasting: succeeds because p points to Child
Compiler Verification vs. Runtime Verification
- Compile-Time Check: The compiler checks if the reference type and the target cast type reside in the same inheritance tree. If the two types are unrelated classes, compilation fails immediately with
incompatible types. Casts to an interface type usually compile, because some subclass could implement the interface; the exception is afinalclass that does not implement it, which the compiler rejects. - Runtime Check: The JVM inspects the heap object. If the actual object is not an instance of the target type (or one of its subtypes), the JVM aborts and throws
ClassCastException.
class Bird {}
class Eagle extends Bird {}
class Ostrich extends Bird {}
public class CastDemo {
public static void main(String[] args) {
Bird b = new Eagle();
Eagle e = (Eagle) b; // SUCCESS: b points to an Eagle
Ostrich o = (Ostrich) b; // COMPILES, but throws ClassCastException at runtime!
// String s = (String) b; // COMPILE ERROR: inconvertible types (Bird and String are unrelated)
}
}
Consider the following code:
class Alpha {
String label = "AlphaField";
static void printName() { System.out.print("AlphaStatic "); }
void show() { System.out.print("AlphaInstance "); }
}
class Beta extends Alpha {
String label = "BetaField";
static void printName() { System.out.print("BetaStatic "); }
void show() { System.out.print("BetaInstance "); }
}
public class HierarchyDemo {
public static void main(String[] args) {
Alpha ref = new Beta();
System.out.print(ref.label + " ");
ref.printName();
ref.show();
}
}
What is the output when HierarchyDemo is run?
AlphaField BetaStatic BetaInstance
BetaField AlphaStatic AlphaInstance
AlphaField AlphaStatic BetaInstance
BetaField BetaStatic BetaInstance
Given the base class:
import java.io.IOException;
class Storage {
protected CharSequence readData() throws IOException {
return "data";
}
}
Which of the following method declarations is a valid override in a subclass of Storage?
public String readData() throws java.io.FileNotFoundException
private String readData() throws IOException
CharSequence readData() throws Exception
protected Object readData() throws IOException
Consider the following class hierarchy:
class Vehicle {}
class Car extends Vehicle {}
class Motorcycle extends Vehicle {}
public class CastingLab {
public static void main(String[] args) {
Vehicle v = new Car();
Motorcycle m = (Motorcycle) v;
}
}
What happens when this program is compiled and executed?
The code fails to compile because Car cannot be cast to Motorcycle.
The JVM creates a new Motorcycle object by copying common Vehicle fields from Car.
The code executes successfully with m receiving a null reference.
The code compiles successfully but throws a ClassCastException at runtime.
Sections you finish are checked off in the contents.