Method overriding in Java
Same signature, covariant return, cannot reduce visibility or widen checked exceptions.
What overriding is
A subclass overrides a method by declaring one with the same name and parameter types. The new body replaces the inherited one. Change the parameter types and you've created an overload instead — a different method.
Rule 1: return type same or narrower
An override may return the same type or a subtype — a *covariant return*. Dog.self() may return Dog even though Animal.self() returns Animal, because every Dog is an Animal.
class Animal {
Animal self() { return this; }
}
class Dog extends Animal {
@Override Dog self() { return this; }
}Covariant in action
What does this print?
class Shape {
Shape copy() { return this; }
}
class Circle extends Shape {
Circle copy() { return this; }
String n() { return "circle"; }
}
void main() {
System.out.println(new Circle().copy().n());
}shapecircleCompile error: return types differ
Show the answer
circle. Circle copy() is a valid override thanks to the covariant return — and because the result is typed Circle, Circle's own n() can be called directly, no cast needed.
Rule 2: same or more visible
An override can't be less visible than the original. A public method can't become package-private or protected in a subclass. Code holding a parent reference must still be allowed to call it on any subclass object.
class A { public void run() { } }
class B extends A {
void run() { } // error: weaker access
}Rule 3: checked exceptions can't grow
If the parent's method throws IOException, an override may throw IOException, a narrower one (like FileNotFoundException), or nothing — but not a broader one like Exception. Callers prepared for IOException would be ambushed.
class A {
void load() throws IOException { }
}
class B extends A {
@Override
void load() throws Exception { } // error
}Why all these rules?
Parameters: exactly the same. Return: same or subtype. Access: same or more open. Checked exceptions: same, narrower or none. What's the one idea behind all four?
Think about it, then reveal the answer
Substitutability. Anywhere the parent's method was expected, the override must work: it accepts the same arguments, returns something that fits, is callable from the same places, and throws nothing the caller didn't prepare for.
In real projects
The JDK uses these rules itself: Closeable.close() narrows AutoCloseable.close()'s throws Exception down to throws IOException. And when an IDE refuses your override, it's almost always one of these four rules.
Key takeaways
- Same name and parameter types
- Return type: same or a subtype
- Access: same or more open
- Checked exceptions: same, narrower or none
Covariant return types arrived in Java 5. Before that, every clone() override had to return Object, forcing a cast at every call.
Practice questions
What does this print?
class Animal {
Animal self() { return this; }
String n() { return "animal"; }
}
class Dog extends Animal {
@Override Dog self() { return this; }
String n() { return "dog"; }
}
void main() { System.out.println(new Dog().self().n()); }- dog
- animal
- Compile error: return types differ
Check your answer
dog. Overrides may narrow the return type to a subtype (covariant return). Dog.self() is a valid override and n() dispatches to Dog's version.
What does this print?
class A {
public void run() { }
}
class B extends A {
void run() { }
}
void main() { System.out.println("ok"); }- ok
- Compile error
- Nothing is printed
Check your answer
Compile error. B.run() has package access, which is weaker than public. Overrides may not reduce visibility, so this does not compile.