Method Overriding
What Is Overriding?
A subclass overrides an inherited method by declaring a method with the same signature (name plus parameter types) and a covariant or identical return type. The overriding method replaces the superclass implementation for all instances of the subclass.
class Animal {
String speak() { return "..."; }
}
class Dog extends Animal {
@Override
String speak() { return "Woof"; }
}
Key requirements for a valid override:
- Same method name
- Same parameter types (number, order, and types must match exactly)
- Return type must be the same, or a subtype of the original return type (covariant return)
- Access modifier must be at least as permissive (cannot narrow visibility)
throwsclause: the overriding method cannot throw broader checked exceptions than the overridden method
@Override Annotation
The @Override annotation tells the compiler “I intend this method to override a superclass method.” If the signature does not actually match any inherited method, the compiler emits an error.
class Dog extends Animal {
@Override
String speek() { ... } // compile error: no speek() in superclass
}
Without @Override, the misspelled method would silently become a new, unrelated method — no override, no error, just incorrect behaviour. Always use @Override.
Dynamic (Runtime) Dispatch
Method calls are resolved dynamically by the JVM at runtime, using the object’s actual runtime class:
class A {
void speak() { System.out.println("A"); }
}
class B extends A {
@Override
void speak() { System.out.println("B"); }
}
A a = new B();
a.speak(); // prints "B" — dynamic dispatch uses runtime type (B)
Even though a’s declared type is A, the JVM looks up speak() on the object’s actual class (B) and calls B.speak(). This is the foundation of polymorphism.
Access Modifier Rules
An overriding method can make the method more visible but not less:
| Superclass | Valid overrides | Invalid overrides |
|---|---|---|
public | public only | protected, package-private, private |
protected | public, protected | package-private, private |
| package-private | public, protected, package-private | private |
private | Cannot be overridden at all (private methods are not inherited) | N/A |
A private method in the superclass is invisible to the subclass. If the subclass declares a method with the same signature, it is a completely separate method — not an override.
final Methods
A method declared final cannot be overridden by any subclass:
class Animal {
final void vitalFunction() { ... }
}
class Dog extends Animal {
@Override
void vitalFunction() { ... } // compile error
}
Marking a method final communicates that this behaviour is essential to the class’s contract and must not be changed by subclasses. It also allows the JIT compiler to optimise method calls more aggressively.
static Methods Are Hidden, Not Overridden
static methods belong to the class, not instances. If a subclass declares a static method with the same signature, it hides the superclass’s static method — similar to field shadowing:
class A {
static void m() { System.out.println("A.m"); }
}
class B extends A {
static void m() { System.out.println("B.m"); }
}
A a = new B();
a.m(); // prints "A.m" — resolved by declared type (like field access)
Static methods use static (compile-time) resolution, not dynamic dispatch. This is another common Tripos trap.
Covariant Return Types
Since Java 5, the overriding method’s return type can be a subtype of the overridden method’s return type:
class Animal {
Animal reproduce() { return new Animal(); }
}
class Dog extends Animal {
@Override
Dog reproduce() { return new Dog(); } // covariant: Dog is a subtype of Animal
}
This is useful for implementing the prototype pattern or builder-like methods where the caller expects a more specific type.
super.method() — Calling the Overridden Version
Within a subclass, super.methodName(args) calls the superclass’s implementation, bypassing the subclass’s override:
class Dog extends Animal {
@Override
void eat() {
super.eat(); // do the Animal thing
wagTail(); // then do the Dog thing
}
}
This lets you extend behaviour rather than completely replace it.
Overriding vs Overloading
| Overriding | Overloading | |
|---|---|---|
| Where | Subclass (same hierarchy) | Same class or subclass |
| Signature | Same name, same parameter types | Same name, different parameter types |
| Return type | Same or covariant | Can be different |
| Resolution | Runtime (dynamic dispatch) | Compile time (static resolution) |
| Annotation | @Override | No special annotation |
| Access modifier | Cannot narrow | No restriction |
Overloading a method that is also overridden can create confusing resolution. The compiler picks the overload based on argument types; the JVM picks the overriding implementation based on the runtime object type. Both mechanisms are involved in a single call.