What is Method Overriding?
Method Overriding is a fundamental concept in Object-Oriented Programming that allows a subclass to provide a specific implementation of a method that is already defined in its superclass. This is a key mechanism for achieving runtime polymorphism, where the actual method that gets executed is determined by the object's type at runtime, not at compile time.
When a subclass provides its own implementation of a method from a superclass, it overrides the superclass's version. This enables the subclass to customize behavior while maintaining the same interface as the parent class.
There are two main types of polymorphism in Java:
- Compile-time Polymorphism: Method calls are resolved at compile time based on the reference type.
- Runtime Polymorphism: Method calls are resolved at runtime based on the actual object's type. This is what enables true OOP flexibility.
The Contract of Overriding
Java has strict rules for method overriding. Violating these rules will result in a compile-time error. Understanding these rules is crucial for effective OOP design.
Visualizing the Difference
Let's visualize the difference between compile-time and runtime polymorphism with a simple analogy:
Compile-time Polymorphism
Method calls are resolved at compile time based on the reference type.
Animal myAnimal = new Dog(); // Reference type is Animal
myAnimal.makeSound(); // Calls Dog's makeSound() method
Runtime Polymorphism
Method calls are resolved at runtime based on the actual object's type.
Animal myAnimal = new Dog(); // Reference type is Animal
Animal[] animals = {myAnimal, new Cat()}; // Array of Animal references
for (Animal animal : animals) {
animal.makeSound(); // Calls the appropriate method based on actual object type
}
In the first case, the compiler knows myAnimal is a Dog and calls makeSound() at compile time. In the second case, the actual type of each object in the array is determined at runtime, and the appropriate method is called.
| Rule | Description | Example |
|---|---|---|
| Method Signature Must Match | The overriding method must have the same name, parameters, and return type as the method in the superclass. | class Parent { void display() {...} class Child extends Parent { @Override void display() {...} |
| Access Modifiers Cannot Be More Restrictive | The overriding method cannot have a more restrictive access modifier than the overridden method (e.g., can't change from public to protected). |
class Parent { public void display() {...} class Child extends Parent { @Override protected void display() {...} |
| Checked Exceptions | If the overridden method in the superclass throws a checked exception, the overriding method must either throw the same exception or a subclass of it. | class Parent { public void save() throws IOException {...} class Child extends Parent { @Override public void save() throws FileNotFoundException {...} |
Use of super Keyword |
To call the superclass's implementation, use the super keyword. This is required when you want to extend the behavior rather than completely replace it. |
class Child extends Parent { @Override public void display() { super.display(); /* additional behavior */ } |
Putting Theory into Practice
Let's look at a complete example that demonstrates method overriding in action:
// Superclass
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void makeSound() {
System.out.println(name + " makes a sound");
}
public void displayInfo() {
System.out.println("Animal: " + name);
}
}
// Subclass that overrides makeSound()
class Dog extends Animal {
private String breed;
public Dog(String name, String breed) {
super(name);
this.breed = breed;
}
@Override
public void makeSound() {
System.out.println(getName() + " barks");
}
@Override
public void displayInfo() {
// Call to parent method and add subclass-specific information
super.displayInfo();
System.out.println("Breed: " + breed);
}
}
// Subclass that overrides makeSound()
class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(getName() + " meows");
}
@Override
public void displayInfo() {
// Call to parent method and add subclass-specific information
super.displayInfo();
System.out.println("This is a cat");
}
}
// Demonstration of runtime polymorphism
class PolymorphismExample {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // Reference type is Animal
Animal[] animals = {myAnimal, new Cat()}; // Array of Animal references
System.out.println("--- Using compile-time polymorphism ---");
myAnimal.makeSound(); // Calls Dog's makeSound() method
System.out.println("\n--- Using runtime polymorphism ---");
for (Animal animal : animals) {
animal.makeSound(); // Calls the appropriate method based on actual object type
}
}
}
Making Your Intent Clear
The @Override annotation is not required for method overriding, but it's a best practice to use it. It tells the compiler that you intend to override a method from the superclass, which helps catch errors at compile time and improves code readability.
Without the annotation, if you make a mistake in the method signature (e.g., misspell the method name), the compiler will treat it as a new method rather than an override, leading to subtle bugs.
// Without @Override - this creates a new method
class Cat extends Animal {
public Cat(String name) {
super(name);
}
// Misspelled method name - this doesn't override
public void makeSound() {
System.out.println(getName() + " meows");
}
// Misspelled method name - this doesn't override
public void displayInfo() {
System.out.println("Cat: " + getName());
}
}
// With @Override - clearly indicates intent
class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override // Clearly indicates we're overriding the parent method
public void makeSound() {
System.out.println(getName() + " meows");
}
@Override // Clearly indicates we're overriding the parent method
public void displayInfo() {
super.displayInfo();
System.out.println("This is a cat");
}
}
Adapting Return Types
Covariant return types refer to the ability of an overriding method to return a subtype of the return type of the overridden method. In Java, this is allowed, which provides more flexibility in inheritance hierarchies.
For example, if a superclass method returns an Animal, a subclass can override it to return a more specific type like Dog. This maintains type safety while enabling polymorphism.
class Animal {
// Returns a generic Animal
public Animal createAnimal() {
return new Animal("Generic Animal");
}
}
class Dog extends Animal {
// Returns a more specific Dog
@Override
public Dog createDog() {
return new Dog("Generic Dog");
}
}
class CovariantExample {
public static void main(String[] args) {
Animal animal = new Dog().createDog();
// The actual object is a Dog, but the reference type is Animal
Animal genericAnimal = animal;
// This is safe because Dog IS-A Animal
System.out.println("Created a: " + genericAnimal.getClass().getSimpleName());
}
}
When Subclasses Define Same Methods
Method Hiding occurs when a subclass defines a static method with the same signature as a static method in the superclass. This is different from method overriding, as it's based on scope rather than polymorphism.
When you call a static method on a subclass reference, the method that gets executed depends on the declared type of the reference, not the actual object's type.
class Parent {
public static void staticMethod() {
System.out.println("Parent's static method");
}
}
class Child extends Parent {
// This hides the parent's static method
public static void staticMethod() {
System.out.println("Child's static method");
}
public static void main(String[] args) {
Parent parent = new Child();
parent.staticMethod(); // Calls Child's version
}
}
Runtime Polymorphism in Action
Dynamic Method Dispatch is the mechanism by which Java determines which version of an overridden method to execute at runtime. The decision is based on the actual type of the object, not the reference type.
This is what enables true polymorphism in Java. You can write code that operates on a superclass type, but at runtime, the specific implementation of the actual object's type is called.
class Animal {
public void makeSound() {
System.out.println("Some animal sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
}
class DynamicDispatch {
public static void main(String[] args) {
Animal myDog = new Dog(); // Reference type is Animal
Animal myCat = new Cat(); // Reference type is Animal
Animal[] animals = {myDog, myCat}; // Array of Animal references
System.out.println("--- Using compile-time polymorphism ---");
myDog.makeSound(); // Calls Dog's implementation
System.out.println("\n--- Using runtime polymorphism ---");
for (Animal animal : animals) {
animal.makeSound(); // Calls the appropriate method based on actual object type
}
}
}
Special Considerations
When a method is declared as final in a superclass, it cannot be overridden by subclasses. This is a deliberate design choice to prevent critical methods from being changed.
However, private methods in a superclass can be effectively overridden if a subclass defines a new method with the same signature. This is because the subclass cannot see or call the superclass's private methods, so it's not actually overriding but providing a new implementation.
class Parent {
// This method cannot be overridden
public final void criticalOperation() {
System.out.println("Performing critical operation");
}
}
class Child extends Parent {
// This is not an override, but provides a new implementation
public void criticalOperation() {
System.out.println("Child's safer implementation of critical operation");
}
}
Test Your Skills
- Create a class hierarchy for different types of vehicles (e.g.,
Vehicle,Car,ElectricCar). - Override the
start()method in each subclass to provide specific behavior. - Use dynamic method dispatch to demonstrate polymorphism. Create an array of different Vehicle types and call their
start()methods. - Create a class with a
finalmethod that cannot be overridden, and demonstrate how a subclass can still provide additional functionality.
🏆 Challenge: Advanced Shape System
Create an advanced shape system that demonstrates multiple inheritance concepts:
- Create an abstract
Shapeclass with afinalcalculateArea()method. - Create multiple concrete shape classes (
Circle,Rectangle,Traingle) that extendShape. - Override
calculateArea()in each subclass to provide specific implementations. - Create a
ShapeCalculatorclass that can work with anyShapeobject and demonstrates dynamic method dispatch. - Add a method to calculate the total area of multiple shapes, demonstrating polymorphism.
- Add a method to calculate the total area of multiple shapes, demonstrating polymorphism.
abstract class Shape {
protected String name;
protected double area;
public Shape(String name) {
this.name = name;
}
public String getName() { return name; }
public double getArea() { return area; }
// Cannot be overridden
public final void calculateArea() {
// Implementation here
area = Math.PI * 10 * 10; // Example calculation
System.out.println("Calculating area for " + name);
}
}
class Circle extends Shape {
private double radius;
public Circle(double radius) {
super("Circle");
this.radius = radius;
}
@Override
public void calculateArea() {
area = Math.PI * radius * radius;
System.out.println("Calculating area for Circle");
}
@Override
public double getArea() {
return area;
}
}
class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
super("Rectangle");
this.width = width;
this.height = height;
}
@Override
public void calculateArea() {
area = width * height;
System.out.println("Calculating area for Rectangle");
}
@Override
public double getArea() {
return area;
}
}
class Triangle extends Shape {
private double base, height;
public Triangle(double base, double height) {
super("Triangle");
this.base = base;
this.height = height;
}
@Override
public void calculateArea() {
area = 0.5 * base * height;
System.out.println("Calculating area for Triangle");
}
@Override
public double getArea() {
return area;
}
}
class ShapeCalculator {
public double calculateTotalArea(Shape[] shapes) {
double totalArea = 0;
for (Shape shape : shapes) {
shape.calculateArea(); // Dynamic method dispatch
totalArea += shape.getArea();
}
return totalArea;
}
public void printAreas(Shape[] shapes) {
for (Shape shape : shapes) {
System.out.printf("%s area: %.2f%n", shape.getName(), shape.getArea());
}
}
}
public class AdvancedShapeSystem {
public static void main(String[] args) {
Shape[] shapes = {
new Circle(5.0),
new Rectangle(10.0, 8.0),
new Triangle(6.0, 8.0)
};
ShapeCalculator calculator = new ShapeCalculator();
System.out.println("--- Individual Areas ---");
calculator.printAreas(shapes);
System.out.printf("%nTotal Area: %.2f%n", calculator.calculateTotalArea(shapes));
}
}