Method Overriding in Java

Understand how child classes customize parent methods to achieve runtime polymorphism and flexible designs.

What is Method Overriding?

Method overriding happens when a subclass (child) provides its own specific implementation of a method that already exists in its superclass (parent). The child class essentially replaces the parent's version with its own custom behavior while keeping the method signature identical.

Imagine a parent teaching all children to "introduce yourself." Each child follows the same instruction but introduces themselves differently based on their personality. Same action, different execution.

  • Requires inheritance relationship between classes
  • Child method must match parent's method signature exactly
  • Enables runtime polymorphism (dynamic method dispatch)
  • Provides specialized behavior while maintaining a common interface

💡 Quick Distinction: Overloading = same class, different parameters. Overriding = parent-child relationship, identical signature, different behavior.

Basic Overriding Example

See how different animal types override the same method to produce unique behaviors.

Example: Animal Sound Override

// Parent class
class Animal {
    void makeSound() {
        System.out.println("Some generic animal sound");
    }
}

// Child class 1
class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Woof! Woof!");
    }
}

// Child class 2
class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("Meow! Meow!");
    }
}

// Child class 3
class Cow extends Animal {
    @Override
    void makeSound() {
        System.out.println("Moo! Moo!");
    }
}

public class OverrideDemo {
    public static void main(String[] args) {
        Animal myDog = new Dog();
        Animal myCat = new Cat();
        Animal myCow = new Cow();
        
        myDog.makeSound();  // Woof! Woof!
        myCat.makeSound();  // Meow! Meow!
        myCow.makeSound();  // Moo! Moo!
    }
}
Output:
Woof! Woof!
Meow! Meow!
Moo! Moo!

Explanation:

  • Each animal class extends Animal and overrides makeSound()
  • @Override annotation ensures we're correctly overriding (optional but recommended)
  • All animals share the same method name but execute different code
  • Parent reference can hold child objects (polymorphism)

Runtime Polymorphism in Action

The true power of overriding: Java decides which method to execute at runtime based on the actual object type, not the reference type.

Example: Dynamic Method Dispatch

class Shape {
    void draw() {
        System.out.println("Drawing a shape");
    }
    
    void area() {
        System.out.println("Calculating area");
    }
}

class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a circle ⭕");
    }
    
    @Override
    void area() {
        System.out.println("Area = π × r²");
    }
}

class Rectangle extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a rectangle ▭");
    }
    
    @Override
    void area() {
        System.out.println("Area = length × width");
    }
}

public class PolymorphismExample {
    public static void main(String[] args) {
        // Parent reference, child objects
        Shape shape1 = new Circle();
        Shape shape2 = new Rectangle();
        Shape shape3 = new Shape();
        
        // Runtime decides which method to call
        shape1.draw();   // Circle's draw()
        shape1.area();   // Circle's area()
        
        shape2.draw();   // Rectangle's draw()
        shape2.area();   // Rectangle's area()
        
        shape3.draw();   // Shape's draw()
        shape3.area();   // Shape's area()
    }
}
Output:
Drawing a circle ⭕
Area = π × r²
Drawing a rectangle ▭
Area = length × width
Drawing a shape
Calculating area

Explanation:

  • Reference type is Shape, but actual objects are Circle and Rectangle
  • JVM determines the actual object type during runtime (not compile time)
  • Method called depends on object type, not reference type
  • This flexibility is the foundation of polymorphic behavior

Essential Rules for Method Overriding

1. Exact Method Signature

Method name, parameter list, and return type must be identical to the parent method.

Rule: Even one parameter difference makes it overloading, not overriding

2. Inheritance is Mandatory

Only possible when child class extends parent class (IS-A relationship).

Syntax: class Child extends Parent

3. Access Level Cannot Decrease

Child's method access must be same or more permissive than parent's.

Valid: protected → public | Invalid: public → private

4. Cannot Override Static Methods

Static methods belong to the class, not instances. They're hidden, not overridden.

Note: You can have same static method name in child, but it's method hiding

5. Cannot Override Final Methods

Methods marked as final are locked and cannot be changed by subclasses.

Purpose: final prevents modification of critical method behavior

6. Private Methods Not Inherited

Private methods aren't visible to child classes, so they can't be overridden.

Result: Creating same-named method in child is a new method, not override

The @Override Annotation

Use @Override to let the compiler verify you're actually overriding. It catches typos and signature mismatches at compile time.

Example: Benefits of @Override

class Vehicle {
    void startEngine() {
        System.out.println("Engine starting...");
    }
}

class Car extends Vehicle {
    // Correct override
    @Override
    void startEngine() {
        System.out.println("Car engine started with ignition key");
    }
    
    // Typo in method name - @Override catches this error!
    // @Override
    // void startEngien() {  // Compile error: method doesn't exist in parent
    //     System.out.println("Typo version");
    // }
    
    // Wrong parameters - @Override catches this too!
    // @Override
    // void startEngine(String key) {  // Compile error: signature mismatch
    //     System.out.println("With key: " + key);
    // }
}

public class AnnotationDemo {
    public static void main(String[] args) {
        Vehicle myCar = new Car();
        myCar.startEngine();
    }
}
Output:
Car engine started with ignition key

Explanation:

  • @Override tells compiler to verify this method overrides a parent method
  • Prevents accidental creation of new methods due to typos
  • Makes code intention clear and improves maintainability
  • Optional but considered best practice in professional code

Using super to Call Parent Methods

Sometimes you want to extend parent functionality rather than completely replace it. Use super keyword to call the parent's version.

Example: Extending Parent Behavior

class Employee {
    String name;
    double salary;
    
    Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }
    
    void displayInfo() {
        System.out.println("Name: " + name);
        System.out.println("Salary: $" + salary);
    }
}

class Manager extends Employee {
    String department;
    
    Manager(String name, double salary, String department) {
        super(name, salary);
        this.department = department;
    }
    
    @Override
    void displayInfo() {
        // Call parent's method first
        super.displayInfo();
        // Add additional information
        System.out.println("Department: " + department);
        System.out.println("Role: Manager");
    }
}

public class SuperExample {
    public static void main(String[] args) {
        Manager mgr = new Manager("Alice Johnson", 75000, "IT");
        mgr.displayInfo();
    }
}
Output:
Name: Alice Johnson
Salary: $75000.0
Department: IT
Role: Manager

Explanation:

  • super.displayInfo() calls the parent's version of the method
  • Allows code reuse without duplicating parent's logic
  • Child method extends functionality instead of replacing entirely
  • super is required to explicitly invoke parent's overridden method

Real-World Example: Payment System

See how overriding enables flexible payment processing for different payment methods.

Example: Payment Processing

class Payment {
    double amount;
    
    Payment(double amount) {
        this.amount = amount;
    }
    
    void processPayment() {
        System.out.println("Processing payment of $" + amount);
    }
    
    void generateReceipt() {
        System.out.println("Receipt generated for $" + amount);
    }
}

class CreditCardPayment extends Payment {
    String cardNumber;
    
    CreditCardPayment(double amount, String cardNumber) {
        super(amount);
        this.cardNumber = cardNumber;
    }
    
    @Override
    void processPayment() {
        System.out.println("Processing credit card payment...");
        System.out.println("Card: **** **** **** " + cardNumber.substring(12));
        System.out.println("Amount: $" + amount);
        System.out.println("Payment approved ✓");
    }
}

class PayPalPayment extends Payment {
    String email;
    
    PayPalPayment(double amount, String email) {
        super(amount);
        this.email = email;
    }
    
    @Override
    void processPayment() {
        System.out.println("Processing PayPal payment...");
        System.out.println("Account: " + email);
        System.out.println("Amount: $" + amount);
        System.out.println("Payment sent via PayPal ✓");
    }
}

class CashPayment extends Payment {
    CashPayment(double amount) {
        super(amount);
    }
    
    @Override
    void processPayment() {
        System.out.println("Processing cash payment...");
        System.out.println("Amount received: $" + amount);
        System.out.println("Thank you! ✓");
    }
}

public class PaymentSystem {
    public static void main(String[] args) {
        Payment payment1 = new CreditCardPayment(150.00, "1234567890123456");
        Payment payment2 = new PayPalPayment(75.50, "user@email.com");
        Payment payment3 = new CashPayment(50.00);
        
        payment1.processPayment();
        System.out.println();
        
        payment2.processPayment();
        System.out.println();
        
        payment3.processPayment();
    }
}
Output:
Processing credit card payment...
Card: **** **** **** 3456
Amount: $150.0
Payment approved ✓

Processing PayPal payment...
Account: user@email.com
Amount: $75.5
Payment sent via PayPal ✓

Processing cash payment...
Amount received: $50.0
Thank you! ✓

Explanation:

  • Each payment type overrides processPayment() with its own logic
  • System can handle any payment type through parent reference
  • Easy to add new payment methods without changing existing code
  • Demonstrates real-world polymorphism for flexible system design

Covariant Return Types

Java allows changing return type when overriding, as long as the new type is a subclass of the original. This is called covariant return type.

Example: Covariant Return Type

class Animal {
    Animal getBaby() {
        System.out.println("Returning an animal baby");
        return new Animal();
    }
}

class Dog extends Animal {
    @Override
    Dog getBaby() {  // Return type changed from Animal to Dog
        System.out.println("Returning a puppy");
        return new Dog();
    }
}

class Cat extends Animal {
    @Override
    Cat getBaby() {  // Return type changed from Animal to Cat
        System.out.println("Returning a kitten");
        return new Cat();
    }
}

public class CovariantExample {
    public static void main(String[] args) {
        Animal animal = new Animal();
        Animal baby1 = animal.getBaby();
        
        Dog dog = new Dog();
        Dog puppy = dog.getBaby();  // Returns Dog, not just Animal
        
        Cat cat = new Cat();
        Cat kitten = cat.getBaby();  // Returns Cat, not just Animal
        
        System.out.println("\nAll babies created successfully!");
    }
}
Output:
Returning an animal baby
Returning a puppy
Returning a kitten

All babies created successfully!

Explanation:

  • Parent returns Animal, children return Dog/Cat (subclasses of Animal)
  • Valid because Dog and Cat have IS-A relationship with Animal
  • Provides more specific return types in subclasses
  • Feature introduced in Java 5 for better type safety

Overloading vs Overriding Comparison

Method Overloading

Same method name, different parameters within the same class. Resolved at compile time.

Example: print(int x), print(String s), print(int x, int y)

Method Overriding

Same method signature in parent and child classes. Resolved at runtime based on object type.

Example: Parent and child both have void display()

Key Differences

Overloading = compile-time polymorphism, same class. Overriding = runtime polymorphism, inheritance needed.

Purpose: Overloading for convenience, overriding for specialized behavior

Common Overriding Mistakes

  • Parameter mismatch: Different parameters create overloading, not overriding
  • Missing @Override: Typos can accidentally create new methods instead
  • Reducing access: Cannot make public method protected/private in child
  • Trying to override static: Static methods are hidden, not overridden
  • Final method override: Cannot override methods marked as final
  • Private method confusion: Private methods aren't inherited, so can't be overridden
  • Constructor confusion: Constructors cannot be overridden (not inherited)

⚠️ Critical Rule: For overriding, method signature (name + parameters + return type) must match exactly. Even changing one parameter type makes it overloading!

Key Takeaways

  • Definition: Child class provides specific implementation of parent's method
  • Purpose: Enable runtime polymorphism and specialized behavior
  • Requirement: Inheritance (IS-A relationship) is mandatory
  • Signature: Must match exactly - name, parameters, return type
  • @Override: Always use annotation to catch errors early
  • Runtime decision: JVM picks method based on actual object, not reference
  • Access levels: Can widen (protected to public) but not narrow
  • super keyword: Access parent's overridden method when needed

🎯 Best Practice: Always use @Override annotation. It's your safety net against bugs and makes your code intention crystal clear to other developers!