Java Method Overriding â Syntax, Rules, Examples & Best Practices
Everything you need to know about Java Method Overriding â rules, @Override annotation, super keyword, covariant return types, dynamic method dispatch, abstract and interface method overriding, anti-patterns, and real-world production code examples.
Last Updated
March 2026
Read Time
24 min
Level
Intermediate
Chapter
22 of 35
What is Method Overriding in Java?
Method overriding in Java is a feature of object-oriented programming that allows a subclass to provide its own specific implementation of a method already defined in its parent class (superclass). The overriding method in the subclass must have the same name, same parameter list, and a compatible return type as the method in the parent class.
Method overriding is the mechanism that enables runtime polymorphism (also called dynamic polymorphism) in Java. When you call an overridden method on a reference variable, the JVM decides at runtime â based on the actual object type, not the reference type â which version of the method to execute. This is called Dynamic Method Dispatch and is the foundation of flexible, extensible object-oriented design.
Real-world analogy: Consider a general Animal class with a sound() method. A Dog subclass overrides sound() to print "Woof", and a Cat subclass overrides it to print "Meow". When you call sound() on an Animal reference that actually holds a Dog object, Java calls Dog's version â not Animal's. This is overriding in action.
Method Overriding â Syntax & Basic Example
To override a method, the subclass defines a method with the exact same signature (name + parameter list) as the parent class method. The @Override annotation above the method signals the intent to the compiler and is strongly recommended.
class Parent { returnType methodName(params) { // parent implementation } } class Child extends Parent { @Override returnType methodName(params) { // child-specific implementation } }
1. Same method name and parameter list as parent. 2. Return type must be same or a subtype (covariant). 3. Access modifier must be same or less restrictive. 4. Cannot throw broader checked exceptions. 5. Use @Override annotation â always.
Parent reference â Child object â method called â JVM checks actual object type at runtime â executes Child's version. This is Dynamic Method Dispatch. The decision is made at runtime, not compile time.
// Parent class
class Animal {
String name;
Animal(String name) {
this.name = name;
}
// Method to be overridden
public void sound() {
System.out.println(name + " makes a generic animal sound.");
}
public void eat() {
System.out.println(name + " is eating.");
}
}
// Subclass 1 â overrides sound()
class Dog extends Animal {
Dog(String name) {
super(name);
}
@Override
public void sound() {
System.out.println(name + " says: Woof! Woof!");
}
}
// Subclass 2 â overrides sound()
class Cat extends Animal {
Cat(String name) {
super(name);
}
@Override
public void sound() {
System.out.println(name + " says: Meow!");
}
}
// Subclass 3 â does NOT override sound() â inherits parent version
class Fish extends Animal {
Fish(String name) {
super(name);
}
// No sound() override â uses Animal's version
}
public class BasicOverriding {
public static void main(String[] args) {
Animal a1 = new Dog("Buddy");
Animal a2 = new Cat("Whiskers");
Animal a3 = new Fish("Nemo");
a1.sound(); // Output: Buddy says: Woof! Woof!
a2.sound(); // Output: Whiskers says: Meow!
a3.sound(); // Output: Nemo makes a generic animal sound. (inherited)
// eat() is not overridden â all use Animal's version
a1.eat(); // Output: Buddy is eating.
a2.eat(); // Output: Whiskers is eating.
}
}Rules of Method Overriding in Java
Java enforces strict rules for method overriding. Violating any of these rules either results in a compile-time error or causes the method to be treated as a new method (not an override). Knowing these rules thoroughly is essential for both writing correct code and clearing Java interviews.
The overriding method must have the EXACT same name and parameter list (number, type, and order of parameters) as the parent class method. If the parameter list differs even slightly, Java treats it as method OVERLOADING, not overriding â creating a new, separate method. @Override annotation catches this mistake at compile time.
The return type must be the same as or a subtype of the parent method's return type (covariant return type â introduced in Java 5). Example: parent returns Animal, child can return Dog (since Dog is a subtype of Animal). Primitive types must match exactly â int cannot be changed to long. void must remain void.
The overriding method's access modifier cannot be MORE restrictive than the parent method. Allowed: public â public, protected â protected or public, default (package) â default, protected, or public. NOT allowed: public â protected (more restrictive). This ensures the Liskov Substitution Principle â a subclass must be usable wherever its parent is expected.
The overriding method CANNOT throw new checked exceptions or broader checked exceptions than the parent method. It CAN throw fewer checked exceptions, narrower (subtype) checked exceptions, or any number of unchecked (runtime) exceptions. Example: if parent throws IOException, child cannot throw Exception (broader), but can throw FileNotFoundException (narrower) or remove it entirely.
static methods: cannot be overridden â only hidden (method hiding). final methods: explicitly marked as non-overridable â compile error if attempted. private methods: not visible to subclasses â subclass defines a completely new method, not an override. Constructors: cannot be overridden â they are not inherited.
Method overriding REQUIRES an IS-A relationship â the subclass must extend (or implement) the class that defines the original method. Overriding cannot happen within the same class (that would be overloading). The @Override annotation on a method that doesn't override anything (no IS-A relation) causes a compile error.
class Parent {
// Various methods demonstrating override rules
public Animal getAnimal() { return new Animal("Generic"); }
protected void display() { System.out.println("Parent display"); }
public void riskyMethod() throws IOException { }
public final void finalMethod() { System.out.println("Cannot override"); }
private void secretMethod() { System.out.println("Private â not inherited"); }
public static void staticMethod(){ System.out.println("Parent static"); }
}
class Child extends Parent {
// â
Rule 2: Covariant return type â Dog is subtype of Animal
@Override
public Dog getAnimal() { return new Dog("Rex"); }
// â
Rule 3: protected â public is LESS restrictive â allowed
@Override
public void display() { System.out.println("Child display"); }
// â
Rule 4: Narrower checked exception â FileNotFoundException â IOException
@Override
public void riskyMethod() throws FileNotFoundException { }
// â COMPILE ERROR: Cannot override final method
// @Override
// public void finalMethod() { }
// âšī¸ This is a NEW method, NOT an override of secretMethod()
private void secretMethod() { System.out.println("Child secret â new method"); }
// âšī¸ This is METHOD HIDING, NOT overriding
public static void staticMethod() { System.out.println("Child static â hidden"); }
// â COMPILE ERROR: Cannot narrow access â public â protected not allowed
// @Override
// protected void display() { } // ERROR: weaker access privileges
}@Override Annotation â Why It Matters
The @Override annotation (introduced in Java 5) is placed above a method to explicitly declare the programmer's intent: "this method is overriding a method from the superclass or interface." It is a compile-time annotation â it does not affect runtime behaviour, but it is one of the most important defensive practices in Java OOP.
class Shape {
public double area() {
return 0.0;
}
public String describe() {
return "I am a shape.";
}
}
class Circle extends Shape {
private double radius;
Circle(double radius) { this.radius = radius; }
// â
Correct override â @Override confirms it
@Override
public double area() {
return Math.PI * radius * radius;
}
// â Bug WITHOUT @Override â typo goes undetected
// This defines a NEW method, does NOT override describe()
public String Describe() { // Capital D â typo!
return "I am a circle.";
}
// result: Shape's describe() is called on Circle objects â silent bug!
// â
Bug CAUGHT WITH @Override â compile error immediately
// @Override
// public String Describe() { ... }
// ERROR: method does not override or implement a method from a supertype
}
class Rectangle extends Shape {
private double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public String describe() {
return "I am a rectangle with width=" + width + " and height=" + height;
}
}
public class OverrideAnnotation {
public static void main(String[] args) {
Shape s = new Circle(7.0);
System.out.println("Area: " + s.area()); // Output: Area: 153.93...
System.out.println(s.describe()); // Output: I am a shape. (bug!)
Shape r = new Rectangle(5.0, 3.0);
System.out.println("Area: " + r.area()); // Output: Area: 15.0
System.out.println(r.describe()); // Output: I am a rectangle...
}
}super Keyword in Method Overriding
The super keyword in the context of method overriding allows a subclass to explicitly call the overridden parent class method from within the overriding method. This is useful when the child class wants to extend (add to) the parent's behaviour rather than completely replace it. super.methodName() always calls the direct parent's version of the method.
class Vehicle {
private String brand;
private int year;
Vehicle(String brand, int year) {
this.brand = brand;
this.year = year;
}
public void displayInfo() {
System.out.println("Brand : " + brand);
System.out.println("Year : " + year);
}
public double calculateTax() {
return 5000.0; // Base road tax
}
}
class Car extends Vehicle {
private int numDoors;
private double engineCC;
Car(String brand, int year, int numDoors, double engineCC) {
super(brand, year); // Call parent constructor
this.numDoors = numDoors;
this.engineCC = engineCC;
}
@Override
public void displayInfo() {
super.displayInfo(); // â
Call parent's displayInfo() first
// Then add Car-specific info
System.out.println("Doors : " + numDoors);
System.out.println("Engine : " + engineCC + " cc");
}
@Override
public double calculateTax() {
double baseTax = super.calculateTax(); // Get parent's base tax
double engineTax = engineCC > 1500 ? 3000.0 : 1000.0;
return baseTax + engineTax; // Extend, don't replace
}
}
class ElectricCar extends Car {
private int batteryKWh;
ElectricCar(String brand, int year, int numDoors, double engineCC, int batteryKWh) {
super(brand, year, numDoors, engineCC);
this.batteryKWh = batteryKWh;
}
@Override
public void displayInfo() {
super.displayInfo(); // Calls Car's displayInfo() which calls Vehicle's
System.out.println("Battery: " + batteryKWh + " kWh");
}
@Override
public double calculateTax() {
// Electric cars get 50% discount on tax
return super.calculateTax() * 0.5;
}
}
public class SuperKeyword {
public static void main(String[] args) {
ElectricCar tesla = new ElectricCar("Tesla", 2024, 4, 0.0, 100);
tesla.displayInfo();
// Output:
// Brand : Tesla
// Year : 2024
// Doors : 4
// Engine : 0.0 cc
// Battery: 100 kWh
System.out.println("Tax: âš" + tesla.calculateTax());
// Output: Tax: âš3000.0 (5000 + 1000 = 6000, * 0.5 = 3000)
}
}Covariant Return Type in Method Overriding
Covariant return type (introduced in Java 5) allows the overriding method in a subclass to return a subtype of the return type declared in the parent class method. Before Java 5, the return type had to be exactly the same. Covariant return types make method overriding more flexible and enable cleaner API design â particularly useful in the Builder pattern, factory methods, and fluent APIs.
// Base animal hierarchy
class Animal {
public String name;
Animal(String name) { this.name = name; }
}
class Dog extends Animal {
String breed;
Dog(String name, String breed) {
super(name);
this.breed = breed;
}
}
// Factory demonstrating covariant return type
class AnimalFactory {
// Returns Animal
public Animal createAnimal() {
return new Animal("Generic Animal");
}
}
class DogFactory extends AnimalFactory {
// â
Covariant return type: Dog (subtype of Animal) â valid override
@Override
public Dog createAnimal() { // Returns Dog instead of Animal
return new Dog("Rex", "Labrador");
}
}
// Real-world: Builder pattern using covariant return type
class PersonBuilder {
protected String name;
protected int age;
public PersonBuilder setName(String name) {
this.name = name;
return this;
}
public PersonBuilder setAge(int age) {
this.age = age;
return this;
}
public String build() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
class EmployeeBuilder extends PersonBuilder {
private String department;
// â
Covariant: returns EmployeeBuilder instead of PersonBuilder
// This allows fluent chaining without casting
@Override
public EmployeeBuilder setName(String name) {
this.name = name;
return this; // Returns EmployeeBuilder, not PersonBuilder
}
@Override
public EmployeeBuilder setAge(int age) {
this.age = age;
return this;
}
public EmployeeBuilder setDepartment(String dept) {
this.department = dept;
return this;
}
@Override
public String build() {
return "Employee{name='" + name + "', age=" + age
+ ", dept='" + department + "'}";
}
}
public class CovariantReturnType {
public static void main(String[] args) {
DogFactory factory = new DogFactory();
Dog dog = factory.createAnimal(); // No cast needed â returns Dog directly
System.out.println(dog.name + " - " + dog.breed);
// Output: Rex - Labrador
String emp = new EmployeeBuilder()
.setName("Ravi") // Returns EmployeeBuilder â no cast!
.setAge(30)
.setDepartment("Engineering")
.build();
System.out.println(emp);
// Output: Employee{name='Ravi', age=30, dept='Engineering'}
}
}Dynamic Method Dispatch â The Heart of Runtime Polymorphism
Dynamic Method Dispatch (DMD) is the mechanism by which Java resolves overridden method calls at runtime, based on the actual type of the object the reference points to â not the declared type of the reference variable. This is the technical underpinning of runtime polymorphism and is what makes Java's OOP so powerful and flexible.
class Shape {
public void draw() {
System.out.println("Drawing a generic shape");
}
public double area() { return 0.0; }
}
class Circle extends Shape {
private double radius;
Circle(double r) { this.radius = r; }
@Override
public void draw() { System.out.println("Drawing a Circle (radius=" + radius + ")"); }
@Override
public double area() { return Math.PI * radius * radius; }
}
class Rectangle extends Shape {
private double w, h;
Rectangle(double w, double h) { this.w = w; this.h = h; }
@Override
public void draw() { System.out.println("Drawing a Rectangle (" + w + "x" + h + ")"); }
@Override
public double area() { return w * h; }
}
class Triangle extends Shape {
private double base, height;
Triangle(double b, double h) { this.base = b; this.height = h; }
@Override
public void draw() { System.out.println("Drawing a Triangle (base=" + base + ")"); }
@Override
public double area() { return 0.5 * base * height; }
}
public class DynamicMethodDispatch {
public static void main(String[] args) {
// Parent reference â child objects
// JVM decides which draw() and area() to call AT RUNTIME
Shape[] shapes = {
new Circle(7.0),
new Rectangle(5.0, 3.0),
new Triangle(6.0, 4.0),
new Circle(3.5)
};
double totalArea = 0;
for (Shape s : shapes) {
s.draw(); // Dynamic dispatch â correct draw() called for each
double a = s.area();
System.out.printf(" Area: %.2f%n", a);
totalArea += a;
}
System.out.printf("Total Area: %.2f%n", totalArea);
// Output:
// Drawing a Circle (radius=7.0) Area: 153.94
// Drawing a Rectangle (5.0x3.0) Area: 15.00
// Drawing a Triangle (base=6.0) Area: 12.00
// Drawing a Circle (radius=3.5) Area: 38.48
// Total Area: 219.42
}
// â
Power of DMD: Add new shape without changing this method
static void renderShape(Shape s) {
s.draw(); // Always calls correct version at runtime
}
}Overriding Abstract Methods
An abstract method is a method declared with the abstract keyword in an abstract class â it has no body (no implementation). Every non-abstract subclass that extends an abstract class must override all abstract methods, or it must itself be declared abstract. Abstract methods enforce a contract: subclasses must provide their own implementation. This is the most explicit form of method overriding in Java.
// Abstract class â cannot be instantiated directly
abstract class PaymentMethod {
protected String ownerName;
protected double balance;
PaymentMethod(String ownerName, double balance) {
this.ownerName = ownerName;
this.balance = balance;
}
// Abstract methods â MUST be overridden by subclasses
public abstract boolean processPayment(double amount);
public abstract String getPaymentType();
public abstract double getTransactionFee(double amount);
// Concrete method â inherited by all, can be overridden optionally
public void displayInfo() {
System.out.println(getPaymentType() + " | Owner: " + ownerName
+ " | Balance: âš" + balance);
}
}
// Concrete subclass 1 â must override all abstract methods
class CreditCard extends PaymentMethod {
private double creditLimit;
CreditCard(String ownerName, double balance, double creditLimit) {
super(ownerName, balance);
this.creditLimit = creditLimit;
}
@Override
public boolean processPayment(double amount) {
double fee = getTransactionFee(amount);
double total = amount + fee;
if (creditLimit >= total) {
creditLimit -= total;
System.out.println("Credit Card payment of âš" + amount + " processed. Fee: âš" + fee);
return true;
}
System.out.println("Credit limit exceeded.");
return false;
}
@Override
public String getPaymentType() { return "Credit Card"; }
@Override
public double getTransactionFee(double amount) { return amount * 0.018; } // 1.8%
}
// Concrete subclass 2
class UPI extends PaymentMethod {
UPI(String ownerName, double balance) { super(ownerName, balance); }
@Override
public boolean processPayment(double amount) {
if (balance >= amount) {
balance -= amount;
System.out.println("UPI payment of âš" + amount + " processed. Fee: âš0");
return true;
}
System.out.println("Insufficient UPI balance.");
return false;
}
@Override
public String getPaymentType() { return "UPI"; }
@Override
public double getTransactionFee(double amount) { return 0.0; } // Free
}
public class AbstractMethodOverriding {
public static void main(String[] args) {
// â Cannot instantiate abstract class
// PaymentMethod p = new PaymentMethod("Ravi", 10000); // ERROR
PaymentMethod cc = new CreditCard("Priya", 0, 50000);
PaymentMethod upi = new UPI("Amit", 20000);
cc.displayInfo();
cc.processPayment(2000);
// Output: Credit Card | Owner: Priya | Balance: âš0.0
// Credit Card payment of âš2000.0 processed. Fee: âš36.0
upi.displayInfo();
upi.processPayment(5000);
// Output: UPI | Owner: Amit | Balance: âš20000.0
// UPI payment of âš5000.0 processed. Fee: âš0
}
}Overriding Interface & Default Methods
When a class implements an interface, it must provide implementations (overrides) for all abstract methods of that interface â unless the class is abstract itself. Java 8 introduced default methods in interfaces â methods with a body. These can optionally be overridden by implementing classes. Java 8 also introduced static interface methods (cannot be overridden) and Java 9 added private interface methods (cannot be overridden).
// Interface with abstract and default methods
interface Drawable {
// Abstract â MUST be overridden by implementing classes
void draw();
void resize(double factor);
// Default method â CAN be overridden, but not required
default String getDescription() {
return "A drawable shape.";
}
// Static method â CANNOT be overridden (belongs to interface)
static void printVersion() {
System.out.println("Drawable interface v2.0");
}
}
interface Colorable {
void setColor(String color);
default String getDescription() {
return "A colorable object.";
}
}
// Class implementing one interface â must override all abstract methods
class Square implements Drawable {
private double side;
private String label;
Square(double side, String label) {
this.side = side;
this.label = label;
}
@Override
public void draw() {
System.out.println("Drawing Square '" + label + "' with side=" + side);
}
@Override
public void resize(double factor) {
side *= factor;
System.out.println("Square resized. New side: " + side);
}
// â
Optionally overrides default method
@Override
public String getDescription() {
return "Square with side=" + side + " and label='" + label + "'";
}
}
// Class implementing MULTIPLE interfaces â must resolve default method conflict
class ColoredCircle implements Drawable, Colorable {
private double radius;
private String color;
ColoredCircle(double radius) { this.radius = radius; }
@Override public void draw() { System.out.println("Drawing " + color + " Circle r=" + radius); }
@Override public void resize(double f) { radius *= f; }
@Override public void setColor(String c) { this.color = c; }
// â
MUST override conflicting default method â two interfaces have getDescription()
// Compiler ERROR if not overridden: 'getDescription()' is inherited from both
@Override
public String getDescription() {
// Can call either interface's version explicitly, or write own
return Drawable.super.getDescription() + " | " + Colorable.super.getDescription();
}
}
public class InterfaceMethodOverriding {
public static void main(String[] args) {
Drawable sq = new Square(5.0, "MySq");
sq.draw();
System.out.println(sq.getDescription());
// Output: Drawing Square 'MySq' with side=5.0
// Square with side=5.0 and label='MySq'
ColoredCircle cc = new ColoredCircle(4.0);
cc.setColor("Red");
cc.draw();
System.out.println(cc.getDescription());
// Output: Drawing Red Circle r=4.0
// A drawable shape. | A colorable object.
}
}Method Overriding vs Method Overloading â Key Differences
Method overriding and method overloading are both forms of polymorphism in Java but they work differently, serve different purposes, and are resolved at different times. Confusing the two is one of the most common mistakes in Java interviews and production code.
class Calculator {
// â
OVERLOADING â same class, same name, different parameters (compile-time)
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
public String add(String a, String b) { // String concatenation
return a + b;
}
}
class ScientificCalculator extends Calculator {
// â
OVERRIDING â same signature as parent, different behaviour (runtime)
@Override
public int add(int a, int b) {
System.out.println("Scientific add called");
return a + b; // Could add logging, precision handling, etc.
}
// This is OVERLOADING in ScientificCalculator (new parameter list)
public double add(double a, double b, int precision) {
double result = a + b;
return Math.round(result * Math.pow(10, precision)) / Math.pow(10, precision);
}
}
public class OverridingVsOverloading {
public static void main(String[] args) {
Calculator c = new Calculator();
Calculator sc = new ScientificCalculator(); // Parent ref, child obj
System.out.println(c.add(2, 3)); // Output: 5 (Calculator's add)
System.out.println(sc.add(2, 3)); // Output: Scientific add called â 5
// Dynamic dispatch: sc is ScientificCalculator at runtime
System.out.println(c.add(2.5, 3.5)); // Overloaded â double version
System.out.println(c.add(1, 2, 3)); // Overloaded â 3-param version
}
}Methods That Cannot Be Overridden in Java
Not all methods in Java can be overridden. Understanding which methods cannot be overridden â and why â is essential for both writing correct code and answering interview questions with confidence.
A method declared final cannot be overridden in any subclass. Attempting to override it causes a compile-time error. Use final when the method's behaviour must remain constant throughout the inheritance hierarchy â e.g., security-critical logic, template method steps that must not be changed. Example: String.intern(), Object.getClass() are final in Java's standard library.
Static methods belong to the class, not instances â they have no runtime polymorphism. If a subclass defines a static method with the same signature, it HIDES the parent's method â this is called method hiding, not overriding. The key difference: which version is called depends on the REFERENCE TYPE (compile-time), not the object type (runtime). @Override on a static method causes a compile error.
Private methods are not visible outside their class and cannot be inherited. If a subclass defines a method with the same name as a parent's private method, it creates a completely NEW method â not an override. @Override on such a method causes a compile error. There is no runtime or compile-time connection between the parent's private method and the subclass method.
Constructors are not inherited and therefore cannot be overridden. Each class defines its own constructors. The super() call in a subclass constructor invokes the parent constructor, but that is chaining, not overriding. A method that looks like a constructor but has a return type is actually a regular method â a common beginner confusion.
class Parent {
// final method â sealed behaviour
public final void finalMethod() {
System.out.println("Parent: final method");
}
// static method â class-level, not instance-level
public static void staticMethod() {
System.out.println("Parent: static method");
}
// private method â invisible to subclasses
private void privateMethod() {
System.out.println("Parent: private method");
}
}
class Child extends Parent {
// â COMPILE ERROR: Cannot override final method
// @Override
// public void finalMethod() { }
// âšī¸ METHOD HIDING â not overriding; @Override would cause compile error
public static void staticMethod() {
System.out.println("Child: static method (hiding)");
}
// âšī¸ NEW method in Child â no relation to Parent's privateMethod()
private void privateMethod() {
System.out.println("Child: new private method");
}
}
public class CannotOverride {
public static void main(String[] args) {
Parent p = new Child();
p.finalMethod(); // Output: Parent: final method
// (final â always Parent's version)
// Static â resolved by REFERENCE TYPE, not object type (hiding, not overriding)
Parent.staticMethod(); // Output: Parent: static method
Child.staticMethod(); // Output: Child: static method (hiding)
// p.staticMethod() â calls Parent's version (reference is Parent)
p.staticMethod(); // Output: Parent: static method
}
}Exception Rules in Method Overriding
Java enforces specific rules about what checked exceptions an overriding method can declare. These rules exist to uphold the Liskov Substitution Principle â if you have a reference to the parent class and call a method, you must be able to handle the exceptions declared in the parent. The child class cannot surprise callers with new, broader checked exceptions.
The overriding method can choose to not throw any checked exception, even if the parent method declared one. This makes the subclass stricter â callers using a child reference don't even need a try-catch for that exception.
The overriding method can declare the exact same checked exception as the parent. Callers can handle it identically whether they have a parent or child reference.
The overriding method can declare a subtype of the parent's checked exception. Example: parent throws IOException â child can throw FileNotFoundException (subtype of IOException). Callers handling IOException still catch FileNotFoundException.
The overriding method can throw any unchecked exception (RuntimeException subclasses) regardless of what the parent declares. RuntimeExceptions do not need to be declared or caught â they propagate freely.
The overriding method CANNOT add a new checked exception not declared in the parent. Example: parent throws IOException â child cannot also throw SQLException. This would break code that only catches IOException when using a parent reference. Compile-time error.
The overriding method CANNOT throw a broader (supertype) checked exception. Example: parent throws FileNotFoundException â child cannot throw IOException (broader). IOException is a supertype of FileNotFoundException â callers handling FileNotFoundException might not catch IOException. Compile-time error.
import java.io.*;
class DataReader {
public String readData() throws IOException {
return "data";
}
}
class FileReader extends DataReader {
// â
Narrower exception â FileNotFoundException â IOException
@Override
public String readData() throws FileNotFoundException {
return "file data";
}
}
class MemoryReader extends DataReader {
// â
No exception at all â stricter than parent
@Override
public String readData() {
return "memory data";
}
}
class NetworkReader extends DataReader {
// â
Same exception as parent
@Override
public String readData() throws IOException {
return "network data";
}
// â
Unchecked exceptions always allowed regardless of parent
// Can also throw NullPointerException, IllegalStateException, etc.
}
class BrokenReader extends DataReader {
// â COMPILE ERROR: Exception is broader â IOException is parent of FileNotFoundException
// (Parent declares IOException; child cannot widen to Exception)
// @Override
// public String readData() throws Exception { return null; }
// â COMPILE ERROR: New checked exception â SQLException not declared in parent
// @Override
// public String readData() throws SQLException { return null; }
}Common Mistakes & Pitfalls â Bugs That Fool Everyone
These mistakes are consistently found in Java code written by developers who understand the concept of overriding but make subtle errors in practice. Each mistake either results in a silent bug (no compile error, wrong runtime behaviour) or a confusing compile error.
class Parent {
public void print(int x) { System.out.println("Parent: " + x); }
public void show() { System.out.println("Parent show"); }
}
class Child extends Parent {
// â MISTAKE 1: Overloading instead of Overriding (different param type)
// This is OVERLOADING (double vs int) â NOT overriding!
// No @Override â no compile error â silent bug: Parent's print(int) still used
public void print(double x) { System.out.println("Child: " + x); }
// â
With @Override: compile error immediately reveals the mistake
// @Override
// public void print(double x) { } // ERROR: method does not override
// â MISTAKE 2: Narrowing access â compile error
// Parent's show() is public; cannot reduce to protected
// @Override
// protected void show() { } // ERROR: weaker access privileges
// â MISTAKE 3: Calling super after modifying state â unexpected double-action
@Override
public void show() {
System.out.println("Child show");
super.show(); // â
Intentional super call is fine
// â But avoid if parent show() has side effects you don't want doubled
}
}
class StringExample {
// â MISTAKE 4: Forgetting @Override on equals() â silent object comparison bug
// This OVERLOADS equals (Object parameter), not OVERRIDES it!
public boolean equals(StringExample other) { // â Wrong parameter type
return this == other;
}
// Object.equals(Object) is still active â old behaviour used in collections!
// â
Correct override: parameter must be Object, not StringExample
// @Override
// public boolean equals(Object obj) { ... }
}
// â MISTAKE 5: Expecting overriding to work on static methods
class Base { public static void greet() { System.out.println("Base greet"); } }
class Derived extends Base { public static void greet() { System.out.println("Derived greet"); } }
class Test {
public static void main(String[] args) {
Base obj = new Derived();
obj.greet(); // Output: Base greet â NOT Derived greet!
// Static methods use reference type (Base), not object type (Derived)
// This is method HIDING, not overriding â no dynamic dispatch
}
}Bad Practices & Anti-Patterns â What Senior Developers Reject
These anti-patterns appear in code reviews and cause long-term maintenance problems. Each one represents a misuse or misunderstanding of method overriding that makes code harder to understand, extend, or debug.
Never write an overriding method without @Override. Without it, typos in method names or wrong parameter types silently create new methods instead of overrides. The bug only surfaces at runtime â often in production. Rule: EVERY method that intends to override MUST have @Override. Configure your IDE's warning settings to treat missing @Override as an error.
When overriding, blindly ignoring the parent's implementation can break functionality the parent was maintaining â particularly logging, validation, event firing, lifecycle callbacks. Always evaluate whether super.methodName() should be called. Frameworks (Spring, Android) frequently require super.onCreate(), super.onDestroy(), etc. Skipping them causes subtle, hard-to-diagnose bugs.
If you override equals() in a class, you MUST also override hashCode(). Java's contract: equal objects must have the same hash code. Breaking this contract causes objects to behave incorrectly in hash-based collections (HashMap, HashSet, Hashtable) â they won't be found even when they 'should' be equal. This is one of the most dangerous and common OOP mistakes in Java.
If a parent constructor calls a method that the child overrides, the child's version runs while the child's fields are still uninitialized (the child constructor hasn't run yet). This leads to NullPointerException or incorrect state. Rule: never call overridable (non-private, non-final, non-static) methods from constructors. Use factory methods, init() methods, or final methods instead.
When overriding methods span 4-5 levels of inheritance hierarchy, understanding what actually runs when you call a method becomes extremely difficult. Each level might call super, modify state, and add behaviour. Document override intentions clearly, keep hierarchies shallow (prefer composition over deep inheritance), and use @Override consistently so the chain is traceable.
Code like 'if (obj instanceof Dog) { ((Dog) obj).bark(); } else if (obj instanceof Cat) { ((Cat) obj).meow(); }' is a sign that method overriding is not being used where it should be. Define a common sound() method in the parent, override it in each subclass, and call obj.sound() â no instanceof needed. instanceof-based dispatch defeats the entire purpose of polymorphism.
Real-World Production Code Examples â Method Overriding in Context
The following examples model method overriding usage in real enterprise Java codebases â Spring Boot application patterns with idiomatic polymorphism at each layer.
package com.techsustainify.notification.service;
// Base notification service â defines the contract
public abstract class NotificationService {
protected String appName;
protected String senderAddress;
public NotificationService(String appName, String senderAddress) {
this.appName = appName;
this.senderAddress = senderAddress;
}
// Abstract â each channel implements its own delivery
public abstract boolean send(String recipientAddress, String subject, String body);
// Abstract â each channel validates its own address format
public abstract boolean isValidAddress(String address);
// Concrete â shared retry logic (can be overridden for channel-specific retry)
public boolean sendWithRetry(String address, String subject, String body, int maxRetries) {
if (!isValidAddress(address)) {
System.out.println("Invalid address: " + address);
return false;
}
for (int attempt = 1; attempt <= maxRetries; attempt++) {
if (send(address, subject, body)) {
System.out.println("Sent on attempt " + attempt);
return true;
}
System.out.println("Attempt " + attempt + " failed. Retrying...");
}
return false;
}
}
// Email channel
public class EmailNotificationService extends NotificationService {
private String smtpHost;
public EmailNotificationService(String appName, String from, String smtpHost) {
super(appName, from);
this.smtpHost = smtpHost;
}
@Override
public boolean send(String recipientEmail, String subject, String body) {
// SMTP email send logic
System.out.println("[EMAIL] To: " + recipientEmail + " | Subject: " + subject);
System.out.println("[EMAIL] Via SMTP: " + smtpHost);
return true;
}
@Override
public boolean isValidAddress(String address) {
return address != null && address.contains("@") && address.contains(".");
}
}
// SMS channel
public class SmsNotificationService extends NotificationService {
private String smsGatewayUrl;
public SmsNotificationService(String appName, String fromNumber, String gatewayUrl) {
super(appName, fromNumber);
this.smsGatewayUrl = gatewayUrl;
}
@Override
public boolean send(String phoneNumber, String subject, String body) {
// SMS send logic â subject not used for SMS
System.out.println("[SMS] To: " + phoneNumber + " | Message: " + body);
System.out.println("[SMS] Via Gateway: " + smsGatewayUrl);
return true;
}
@Override
public boolean isValidAddress(String phone) {
return phone != null && phone.matches("^[+]?[0-9]{10,13}$");
}
// Override retry â SMS has different retry policy (faster, fewer retries)
@Override
public boolean sendWithRetry(String address, String subject, String body, int maxRetries) {
System.out.println("[SMS] Using fast retry policy (max 2 regardless of input)");
return super.sendWithRetry(address, subject, body, Math.min(maxRetries, 2));
}
}
// Orchestrator â works with any NotificationService polymorphically
public class NotificationOrchestrator {
public void notifyUser(NotificationService service, // â Any subtype accepted
String address, String subject, String message) {
boolean sent = service.sendWithRetry(address, subject, message, 3);
System.out.println(sent ? "Notification delivered." : "All attempts failed.");
}
}
class Main {
public static void main(String[] args) {
NotificationOrchestrator orchestrator = new NotificationOrchestrator();
// Same orchestrator works with any channel â polymorphism in action
orchestrator.notifyUser(
new EmailNotificationService("MyApp", "no-reply@myapp.com", "smtp.myapp.com"),
"user@example.com", "Welcome to MyApp", "Thanks for signing up!"
);
orchestrator.notifyUser(
new SmsNotificationService("MyApp", "+919999999999", "https://sms.gateway.in"),
"+918888888888", "", "Your OTP is 482916"
);
}
}Method Overriding Flowchart â How Dynamic Dispatch Works
This flowchart illustrates how the JVM resolves method calls for overridden methods at runtime using Dynamic Method Dispatch.
Code Execution Flow â from source to output
Java Method Overriding Interview Questions â Beginner to Advanced
These questions are consistently asked in Java fresher and experienced-hire interviews, OCPJP/OCA certification exams, and campus placement tests covering OOP concepts.
Practice Questions â Test Your Method Overriding Knowledge
Attempt each question independently before reading the answer â active recall is 2â3x more effective than passive reading for long-term retention.
1. What is the output of the following code? class A { public void greet() { System.out.println("Hello from A"); } } class B extends A { @Override public void greet() { System.out.println("Hello from B"); } } class C extends B { } public class Test { public static void main(String[] args) { A obj = new C(); obj.greet(); } }
Easy2. Will the following code compile? What is the output? class Parent { public void display() { System.out.println("Parent"); } } class Child extends Parent { @Override protected void display() { System.out.println("Child"); } }
Easy3. Find the bug in this code: class Animal { public void makeSound() { System.out.println("Generic sound"); } } class Dog extends Animal { public void MakeSound() { System.out.println("Woof"); } } public class Test { public static void main(String[] args) { Animal a = new Dog(); a.makeSound(); } }
Easy4. What is the output? class Base { public Base getInstance() { System.out.print("Base "); return this; } } class Derived extends Base { @Override public Derived getInstance() { System.out.print("Derived "); return this; } } public class Test { public static void main(String[] args) { Base b = new Derived(); b.getInstance(); } }
Medium5. Will this compile? Explain. class Parent { public void action() throws IOException { } } class Child extends Parent { @Override public void action() throws Exception { } }
Medium6. What is the output? Why? class Parent { public static void print() { System.out.println("Parent static"); } public void show() { System.out.println("Parent show"); } } class Child extends Parent { public static void print() { System.out.println("Child static"); } @Override public void show() { System.out.println("Child show"); } } public class Test { public static void main(String[] args) { Parent p = new Child(); p.print(); p.show(); } }
Medium7. Identify the problem with this code and fix it: class Animal { private String name; public Animal(String name) { this.name = name; logCreation(); } public void logCreation() { System.out.println("Animal created: " + name); } } class Dog extends Animal { private String breed; public Dog(String name, String breed) { super(name); this.breed = breed; } @Override public void logCreation() { System.out.println("Dog created: breed=" + breed); } }
Hard8. Refactor this code using method overriding to eliminate the instanceof checks: void processAnimal(Object animal) { if (animal instanceof Dog) { ((Dog) animal).fetchBall(); } else if (animal instanceof Cat) { ((Cat) animal).purr(); } else if (animal instanceof Bird) { ((Bird) animal).sing(); } }
HardConclusion â Method Overriding: The Engine of Runtime Polymorphism
Method overriding is one of the most fundamental pillars of Java's object-oriented model. It is the mechanism that brings runtime polymorphism to life â allowing you to write code that works with parent class references but automatically executes subclass-specific behaviour at runtime. Without method overriding, inheritance would merely be code reuse; with it, inheritance becomes a powerful tool for building flexible, extensible architectures.
The difference between a junior and senior Java developer is often visible in how they use overriding. Junior code skips @Override, accidentally overloads instead of overrides, calls overridable methods from constructors, and uses instanceof chains where polymorphism belongs. Senior code uses @Override religiously, leverages covariant return types for clean APIs, always overrides hashCode() with equals(), and designs class hierarchies where the instanceof antipattern never appears.
Your next step: Java Abstract Classes â where you'll see how abstract methods formalize method overriding into a contract, forcing all subclasses to provide their own implementations and enabling the most powerful forms of polymorphic design in Java. â