Method Overriding
Method overriding lets a subclass replace a superclass method's implementation while keeping its exact contract — and Java enforces a strict rulebook to make sure that replacement stays safe.
Method overriding is when a subclass provides its own implementation of a method that is already defined, with a compatible signature, in its superclass. At runtime, calling that method on a subclass instance runs the subclass's version instead of the superclass's — this is the mechanism behind dynamic method dispatch (covered in the Polymorphism topic). Overriding is governed by a specific set of compiler-enforced rules.
What Counts as Overriding
To override a method, a subclass must declare a method with the same name, the same parameter types in the same order, and a compatible return type as the superclass method it is replacing — and that superclass method must not be private, static, or final.
Cat's makeSound() matches Animal's makeSound() exactly in name and parameters, so it overrides rather than overloads it.
class Animal {
void makeSound() {
System.out.println("some generic sound");
}
}
class Cat extends Animal {
@Override
void makeSound() { // same name, same (empty) parameter list -- this overrides
System.out.println("meow");
}
}
new Cat().makeSound(); // "meow"Same Method Signature Required
The method signature — name plus parameter types and order — must match exactly. Changing the parameter list creates an overloaded method (a separate method that happens to share a name) instead of an override, which is a very common source of confusion.
Because the parameter types differ (int vs long), Sub.process(long) is a new overloaded method, not an override — calling process(5) through a Base-typed reference still resolves to Base's int version.
class Base {
void process(int x) { System.out.println("Base.process(int)"); }
}
class Sub extends Base {
void process(long x) { System.out.println("Sub.process(long)"); } // overloads, does NOT override
}
Base b = new Sub();
b.process(5); // prints "Base.process(int)" -- Sub's version is never reached hereReturn-Type Covariance
An overriding method's return type does not have to be identical — it may be a covariant return type, meaning any subtype of the original return type. This lets an override return a more specific type than its superclass declared.
Cat.reproduce() returning Cat instead of Animal is legal because Cat is-a Animal — any code expecting an Animal from reproduce() still gets one.
class Animal {
Animal reproduce() {
return new Animal();
}
}
class Cat extends Animal {
@Override
Cat reproduce() { // covariant return: Cat is a subtype of Animal
return new Cat();
}
}Access Modifier Restrictions
An overriding method cannot reduce the visibility of the method it overrides — it may keep the same access level or widen it, but never narrow it. This preserves the guarantee that anything callable through the superclass type remains callable through the subclass type.
If Sub could narrow work() to private, code holding a Base-typed reference to a Sub object could still expect to call work() and would break — Java's rule prevents that.
class Base {
protected void work() { }
}
class Sub extends Base {
// private void work() { } // compile error: cannot reduce visibility
public void work() { } // OK: widening protected -> public
}@Override Annotation
@Override is optional but strongly recommended on every overriding method. It tells the compiler to verify that the method actually overrides something in a superclass (or implements an interface method) — if it doesn't (due to a typo in the name or a mismatched parameter list), the compiler reports an error instead of silently letting a new, unrelated method through.
Without @Override, this typo would compile silently as a brand-new, unrelated method named proccess, and the intended override of process would simply never happen — @Override turns that silent bug into a compile error.
class Base {
void process(int x) { }
}
class Sub extends Base {
@Override
void proccess(int x) { } // compile error: no such method to override (typo caught!)
}final Methods Cannot Be Overridden
Marking a method final in the superclass forbids any subclass from overriding it. This is used when a method's behavior must stay fixed across the whole hierarchy for correctness or safety reasons.
final on criticalStep() is a compile-time guarantee to every caller that this exact behavior runs, regardless of which subclass the object actually is.
class Base {
final void criticalStep() {
System.out.println("must not change");
}
}
class Sub extends Base {
// void criticalStep() { } // compile error: cannot override a final method
}Static Method Hiding — Not Overriding
A static method with the same signature as a superclass's static method does not override it — it hides it. Hiding is resolved at compile time based on the reference's declared type, not the object's runtime type, which is the opposite of how overriding behaves.
Static method calls are resolved using the compile-time type of the reference, unlike instance method overriding, which is resolved using the object's actual runtime type.
class Base {
static void greet() { System.out.println("Base.greet"); }
}
class Sub extends Base {
static void greet() { System.out.println("Sub.greet"); } // hides, does not override
}
Base ref = new Sub();
ref.greet(); // prints "Base.greet" -- resolved by the REFERENCE type at compile time
Sub.greet(); // prints "Sub.greet"private Methods Are Not Overridden
A private method is not visible to subclasses at all, so a subclass declaring a method with the same name and signature is not overriding anything — it is simply a new, unrelated private method, invisible outside its own class.
Because Base.helper() is private, Sub cannot see or override it; run() (declared in Base) always calls Base's own private helper(), never Sub's separately-declared one.
class Base {
private void helper() { System.out.println("Base.helper"); }
void run() { helper(); } // always calls Base's own private helper()
}
class Sub extends Base {
private void helper() { System.out.println("Sub.helper"); } // unrelated method, same name
}
new Sub().run(); // prints "Base.helper" -- Sub's helper() is never involvedChecked Exception Restrictions
An overriding method may not throw a new or broader checked exception than the method it overrides declares. It may throw the same checked exceptions, narrower/subtype checked exceptions, fewer of them, or any unchecked exceptions freely.
Widening the checked exception would break code that catches only IOException around a Base-typed call — the override rule prevents that mismatch from ever compiling.
import java.io.IOException;
import java.io.FileNotFoundException;
class Base {
void load() throws IOException { }
}
class Sub extends Base {
@Override
void load() throws FileNotFoundException { } // OK: FileNotFoundException is-a IOException (narrower)
}
class OtherSub extends Base {
// @Override
// void load() throws Exception { } // compile error: broader checked exception than IOException
}Practical Usage
Overriding toString(), equals(), and hashCode() from Object is the most common overriding scenario in everyday Java code; overriding framework hook methods (e.g. a lifecycle method a base class calls at the right time) is another frequent pattern, and is the mechanism behind template-method-style base classes.
toString() overrides Object's default implementation (which prints a class name and hash code) with something meaningful for this class.
class Product {
private final String name;
private final double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return name + " ($" + price + ")";
}
}Complete Example
Each element calls its own overridden area() through the inherited toString() logic — Shape never needs to know which concrete subtype it is dealing with.
class Shape {
double area() {
return 0.0;
}
@Override
public String toString() {
return "Shape with area " + area();
}
}
class Circle extends Shape {
private final double radius;
Circle(double radius) { this.radius = radius; }
@Override
double area() {
return Math.PI * radius * radius;
}
}
class Square extends Shape {
private final double side;
Square(double side) { this.side = side; }
@Override
double area() {
return side * side;
}
}
public class Main {
public static void main(String[] args) {
Shape[] shapes = { new Circle(2), new Square(3) };
for (Shape s : shapes) {
System.out.println(s); // calls the overridden toString(), which calls the overridden area()
}
}
}Mental Model
Think of overriding as a subclass filling in a superclass's method slot with its own implementation, while the "shape" of that slot — its name, parameter types, and its promised return/exception contract — must stay compatible with what the superclass declared, so any code trusting the superclass's contract keeps working.
Quick Reference
Rule Requirement
---------------------------------- --------------------------------------------------
Method name + parameters must match exactly (same signature)
Return type same type, or a covariant (subtype) return type
Access modifier same or WIDER -- never narrower
final methods cannot be overridden
static methods hidden, not overridden (resolved by reference type)
private methods not inherited, so not overridden either
Checked exceptions same, narrower, or none -- never new/broader ones
@Override optional but recommended -- catches signature mistakesKey Takeaways
Overriding requires an exact match of method name and parameter list; return types may covariantly narrow.
An override cannot reduce the superclass method's access level, and cannot throw new or broader checked exceptions.
final methods cannot be overridden; static methods are hidden, not overridden, and resolve by reference type.
private methods are invisible to subclasses and cannot be overridden.
@Override should be used on every intended override — it turns signature mistakes into compile errors instead of silent bugs.
class Base { void save(String data) { } } class Sub extends Base { void save(Object data) { } // different parameter type -- overloads, not overrides } Base b = new Sub(); b.save("text"); // still calls Base.save(String), not Sub's version class Base { void save(String data) { } } class Sub extends Base { @Override void save(String data) { } // matches exactly -- true override }
Changing the parameter type creates an unrelated overloaded method; @Override would have caught the mismatch immediately as a compile error.
class Base { protected void step() { } } class Sub extends Base { private void step() { } // compile error: cannot reduce visibility } class Base { protected void step() { } } class Sub extends Base { @Override public void step() { } // widening is fine }
Narrowing access on an override is illegal because it would break any code relying on the superclass-level accessibility guarantee.
class Base { static void identify() { System.out.println("Base"); } } class Sub extends Base { static void identify() { System.out.println("Sub"); } } Base ref = new Sub(); ref.identify(); // developer expects "Sub" (thinking it overrides) but gets "Base" class Base { void identify() { System.out.println("Base"); } // instance method: true overriding } class Sub extends Base { @Override void identify() { System.out.println("Sub"); } } Base ref = new Sub(); ref.identify(); // "Sub" -- resolved by runtime type, as expected
Static methods are hidden and resolved by the reference's compile-time type, not overridden and resolved by runtime type; using an instance method is required to get dynamic-dispatch behavior.
import java.io.IOException; class Base { void load() throws IOException { } } class Sub extends Base { @Override void load() throws Exception { } // compile error: broader checked exception } import java.io.IOException; class Base { void load() throws IOException { } } class Sub extends Base { @Override void load() throws IOException { } // same, or a subtype of IOException, is fine }
An override cannot declare a broader checked exception than the method it overrides, since that could break callers who only catch the narrower exception around a Base-typed call.
What must match exactly between an overriding method and the superclass method it overrides?
The method name and the parameter types (including their order) — together, the method signature. The return type may covariantly narrow rather than match exactly.
Can an overriding method reduce the visibility of the method it overrides?
No — it can keep the same access level or widen it, but never narrow it, since code relying on the superclass's declared accessibility must still work through a subclass instance.
Why does @Override help even though it is optional?
It makes the compiler verify that the annotated method truly overrides (or implements) something from a superclass/interface; a typo or mismatched parameter list becomes a compile error instead of silently creating an unrelated new method.
Why can't a final method be overridden?
final on a method is a guarantee that its behavior is fixed across the entire hierarchy; allowing an override would let a subclass silently break that guarantee for anyone relying on it.
What is the difference between overriding a method and hiding a static method?
Overriding applies to instance methods and is resolved at runtime based on the object's actual type (dynamic dispatch). Hiding applies to static methods with a matching signature and is resolved at compile time based on the reference's declared type — calling it through a superclass-typed reference always runs the superclass's version.
Can an overriding method declare a broader checked exception than the superclass method?
No. It may declare the same checked exceptions, narrower (subtype) ones, fewer of them, or none — but never a new or broader checked exception, since that could break callers who only handle the narrower exception type.
Interview-style: why are private methods excluded from overriding?
A private method is not inherited or visible to subclasses at all, so there is nothing for a subclass to "override" — a same-named private method in a subclass is simply a separate, unrelated method scoped to that subclass.
Write a Base class with a method describe() returning a String, and a Sub class overriding it with a covariant return type (a subclass of String is not possible, so instead demonstrate covariance with a custom Animal/Cat reproduce() example).
class Animal { Animal reproduce() { return new Animal(); } } class Cat extends Animal { @Override Cat reproduce() { // covariant return type: Cat is a subtype of Animal return new Cat(); } }
Cat.reproduce() legally overrides Animal.reproduce() while returning the more specific Cat type, because Cat is a subtype of Animal — any caller expecting an Animal back still gets one.
What is printed, and why does a Base-typed reference produce that result?
Prints "Leaf". Instance method calls are resolved by the object's actual runtime type, not the reference's declared type — since b actually refers to a Leaf object, Leaf's overriding greet() runs regardless of what type the reference variable is declared as.
Write a PaymentProcessor base class with a method charge(double amount) throwing a checked PaymentException, and a CreditCardProcessor subclass overriding it to throw a more specific CardDeclinedException that extends PaymentException.
class PaymentException extends Exception { public PaymentException(String msg) { super(msg); } } class CardDeclinedException extends PaymentException { public CardDeclinedException(String msg) { super(msg); } } class PaymentProcessor { void charge(double amount) throws PaymentException { // generic charging logic } } class CreditCardProcessor extends PaymentProcessor { @Override void charge(double amount) throws CardDeclinedException { // narrower checked exception: legal if (amount > 10000) { throw new CardDeclinedException("amount too high"); } } }
CardDeclinedException is a subtype of PaymentException, so declaring it in the override is a narrowing, not a broadening, of the checked exception — this is explicitly allowed by Java's overriding rules.
What does this print, and why doesn't Sub's step() run?
Prints "Base.step". Base.step() is private, so it is not inherited by or visible to Sub at all; Sub's own step() is a completely separate, unrelated private method. run() is defined in Base and always calls Base's own private step() — there is no overriding relationship between the two step() methods.
class Base {
void greet() { System.out.println("Base"); }
}
class Mid extends Base {
@Override
void greet() { System.out.println("Mid"); }
}
class Leaf extends Mid {
@Override
void greet() { System.out.println("Leaf"); }
}
public class Main {
public static void main(String[] args) {
Base b = new Leaf();
b.greet();
}
}class Base {
private void step() { System.out.println("Base.step"); }
void run() { step(); }
}
class Sub extends Base {
private void step() { System.out.println("Sub.step"); }
}
new Sub().run();