8.3 Method Overriding and Polymorphic Method Execution
Key Takeaways
- Method overriding occurs when a subclass provides a specific implementation for an instance method already defined in its superclass using the exact same signature (name and parameter types).
- An overriding method must declare a compatible return type (identical primitive or covariant reference subtype) and cannot assign a more restrictive access modifier.
- Overriding methods cannot declare new or broader checked exceptions than those declared in the superclass method, but may declare fewer checked exceptions or any unchecked exceptions.
- Polymorphism enables a superclass reference variable to point to a subclass object, with the JVM dynamically dispatching method invocations to the overriding implementation at runtime.
- Method overriding enables runtime polymorphism via dynamic method dispatch, whereas method overloading provides compile-time polymorphism resolved by the compiler based on method signatures.
8.3 Method Overriding and Polymorphic Method Execution
[!NOTE] Exam Focus: Polymorphism and method overriding represent the deepest end of Oracle's "describe the components of object-oriented programming" objective on the 1Z0-811 exam. Expect questions that test the exact checklist for valid method overriding (signature matching, covariant returns, access modifier expansion, checked exception limits), dynamic method dispatch mechanics, distinguishing compile-time reference types from runtime object types, and contrasting overriding with method overloading.
In object-oriented programming, polymorphism (originating from Greek meaning "many forms") is the ability of an object to take on many forms. In Java, this capability manifests primarily through subtyping and method overriding: a superclass reference variable can point to an instance of any of its subclasses, and method invocations on that reference automatically trigger the subclass's specialized behavior at runtime.
1. Understanding Method Overriding
Method overriding occurs when a subclass defines an instance method that possesses the exact same method name and the exact same parameter list as an instance method defined in its superclass.
When a method is overridden, the subclass provides its own customized, specialized implementation of an inherited behavior:
public class Animal {
public void makeSound() {
System.out.println("The animal makes a generic sound");
}
}
public class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("The dog barks: Woof! Woof!");
}
}
public class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("The cat meows: Meow!");
}
}
When makeSound() is called on a Dog instance, the JVM executes the Dog version, completely replacing the generic Animal implementation for that object.
2. The Five Strict Rules of Method Overriding
To successfully override a superclass method, the subclass method must satisfy five strict language rules defined in the Java Language Specification. Violating any of these rules results in a compile-time error:
Rule 1: Exact Same Method Signature (Name and Parameters)
The overriding method in the subclass must have the exact same method name and the exact same parameter list (same number, types, and sequence of parameters) as the superclass method.
[!CAUTION] Overriding vs. Overloading Alert: If the parameter list differs in any way (e.g., passing an
intinstead of adouble, or adding an extra argument), you have overloaded the method, not overridden it! Overloading creates a separate method; it does not replace the inherited method.
Rule 2: Compatible Return Types (Covariant Returns)
- Primitive Return Types: The return type must be identical. If the superclass method returns
int, the overriding method must returnint. It cannot returnlong,double, orshort(no primitive type conversions or widening allowed). - Reference Return Types: The return type must be the same type OR a subtype (covariant return type) of the superclass method's return type:
class FoodProducer {
public Object produce() { return new Object(); }
}
class Bakery extends FoodProducer {
// Legal: String is a subclass of Object (Covariant return type)
@Override
public String produce() { return "Fresh Bread"; }
}
If FoodProducer declared public int getRating(), Bakery could not declare public double getRating() because double is not covariant with primitive int.
Rule 3: Access Modifier Cannot Be More Restrictive
The overriding method can maintain the same access level or expand visibility, but can never reduce access (it cannot assign weaker access privileges):
| Superclass Access Modifier | Permissible Overriding Access in Subclass | Forbidden (Compile Error) |
|---|---|---|
public | public | protected, default, private |
protected | protected, public | default, private |
| Default (package-private) | Default, protected, public | private |
class SuperService {
public void serve() { }
}
class SubService extends SuperService {
// COMPILE ERROR: attempting to assign weaker access privileges; was public
@Override
protected void serve() { }
}
Rule 4: Checked Exception Constraints
An overriding method cannot declare new or broader checked exceptions than those declared by the superclass method. However, it is fully permitted to:
- Declare fewer checked exceptions.
- Declare narrower (subclass) checked exceptions.
- Declare no checked exceptions at all.
- Declare any unchecked exceptions (
RuntimeException,Error, or their subclasses).
Rule 5: Non-Overridable Methods
Not all methods can be overridden:
finalMethods: A method markedfinalcannot be overridden under any circumstances. Attempting to override a final method triggers a compile-time error:cannot override; overridden method is final.staticMethods: Static methods belong to the class, not to object instances. A static method cannot be overridden. If a subclass defines a static method with the same signature as a superclass static method, the method is hidden, not overridden (compile-time static binding applies).privateMethods: Private methods are not visible outside their declaring class, and therefore can never be overridden. If a subclass declares a method with the same signature as a private superclass method, it is simply a brand-new unrelated method.
3. The @Override Annotation
The @Override annotation is a compiler directive placed immediately above a method declaration. While optional, it instructs the Java compiler to verify that the annotated method truly satisfies all overriding rules against a superclass method.
public class Shape {
public void draw() { System.out.println("Drawing a shape"); }
}
public class Circle extends Shape {
// Typo in method name: drawe instead of draw
@Override
public void drawe() { // COMPILE ERROR: method does not override or implement a method from a supertype
System.out.println("Drawing a circle");
}
}
Without @Override, the compiler would silently treat drawe() as a completely new method in Circle, leaving draw() un-overridden. Using @Override catches signature mismatches, typos, and improper parameter types at compile time.
4. Polymorphism & Dynamic Method Dispatch in Action
The fundamental operational principle of Java polymorphism can be distilled into one golden rule:
Compile-Time vs. Runtime Separation:
- At Compile Time: The compiler inspects the Reference Type to determine which methods and fields are accessible.
- At Runtime: The JVM inspects the Actual Object Type on the Heap to determine which overriding method implementation to execute.
Dynamic Method Dispatch
When an instance method is called on a reference variable, the JVM uses dynamic method dispatch (runtime polymorphism). The JVM checks the actual object instance on the heap and executes the most specific overriding implementation:
public class ZooApp {
public static void main(String[] args) {
// Superclass reference pointing to a Subclass object!
Animal myPet = new Dog();
// 1. Dynamic Method Dispatch in action:
myPet.makeSound(); // Prints: The dog barks: Woof! Woof!
// 2. Reassigning to a different subclass:
myPet = new Cat();
myPet.makeSound(); // Prints: The cat meows: Meow!
}
}
Even though the reference variable myPet is declared of type Animal, calling myPet.makeSound() invokes Dog.makeSound() and Cat.makeSound() because the underlying objects on the heap are instances of Dog and Cat.
The Reference Type Barrier (Compile-Time Limitation)
What happens if a subclass introduces a brand-new method that does not exist in the superclass?
public class Dog extends Animal {
@Override
public void makeSound() { System.out.println("Woof!"); }
public void fetchBall() { System.out.println("Fetching ball..."); }
}
public class Test {
public static void main(String[] args) {
Animal pet = new Dog();
pet.makeSound(); // LEGAL: makeSound() exists in Animal
// pet.fetchBall(); // COMPILE ERROR: cannot find symbol method fetchBall() in class Animal
}
}
Because the reference variable pet is of type Animal, the compiler only permits calling methods declared in Animal. Even though the underlying object is a Dog, the compiler cannot guarantee at compile time that pet will always hold a Dog. To call fetchBall(), you must explicitly downcast the reference: ((Dog) pet).fetchBall();.
5. Method Overriding vs. Method Overloading: The Definitive Comparison
Distinguishing method overriding from method overloading is one of the most prominent topics on the 1Z0-811 examination:
| Dimension | Method Overriding | Method Overloading |
|---|---|---|
| Defining Context | Across superclass and subclass (inheritance hierarchy) | Within the same class (or inherited from superclass) |
| Method Name | Must be exactly identical | Must be exactly identical |
| Parameter List | Must be identical (same types, count, and order) | Must be different (different types, count, or order) |
| Return Type | Must be identical or covariant (subtype) | Can be identical or completely different |
| Access Modifier | Cannot be more restrictive | Can be identical or completely different |
| Checked Exceptions | Cannot declare new or broader checked exceptions | Can declare any checked or unchecked exceptions |
| Polymorphic Nature | Runtime Polymorphism (Dynamic Method Dispatch) | Compile-Time Polymorphism (Static Early Binding) |
| Resolved By | JVM at runtime based on actual object type | Compiler at compile time based on argument types |
| Annotation | Evaluated with @Override | Does NOT use @Override |
6. Method Hiding: Instance Methods vs. Static Methods
A critical exam question involves declaring a static method in a subclass with the same signature as a static method in its superclass:
class Parent {
public static void display() {
System.out.println("Parent static display");
}
}
class Child extends Parent {
public static void display() {
System.out.println("Child static display");
}
}
public class Main {
public static void main(String[] args) {
Parent ref = new Child();
ref.display(); // What prints?
}
}
Output: Parent static display!
Why Doesn't Child.display() Execute?
Because display() is declared static, it is hidden, not overridden. Static methods do not participate in dynamic method dispatch. The Java compiler resolves static method calls at compile time based strictly on the declared reference type (Parent), translating ref.display() into Parent.display(). Only non-static instance methods are dispatched dynamically at runtime based on the heap object type!
Consider the following Java program:
What is the output when this program is executed?class Shape {
public void draw() {
System.out.print("Shape ");
}
}
class Circle extends Shape {
@Override
public void draw() {
System.out.print("Circle ");
}
}
public class Test {
public static void main(String[] args) {
Shape s = new Circle();
s.draw();
}
}
Consider the following class hierarchy:
What is the compilation result of attempting to compile class SuperService {
public Object getService() { return new Object(); }
public int getPort() { return 8080; }
}
class SubService extends SuperService {
// Member 1:
@Override
public String getService() { return "HTTP"; }
// Member 2:
@Override
public long getPort() { return 8080L; }
}
SubService?
Examine the following code declarations:
What is the result of attempting to compile class Writer {
protected void writeMessage() {
System.out.println("Writing");
}
}
class SpecialWriter extends Writer {
@Override
void writeMessage() {
System.out.println("Special Writing");
}
}
SpecialWriter?