Skip to main content
Create your own
Lesson illustration

Method Overriding for Polymorphism

Hello! Welcome to your fourth lesson in the OOP Foundations module.

In our last lesson, we explored how to build class hierarchies using inheritance, abstract classes, and interfaces. We touched upon the idea that a subclass can provide its own implementation for a method it inherits from a parent. This is called method overriding, and it's the key to one of OOP's most powerful concepts.

Today, our learning outcome is to implement runtime polymorphism using method overriding. We'll unpack what "polymorphism" means, see how it's achieved in Java, and understand why it's a cornerstone of flexible and maintainable system design—a skill essential for your LLD interview preparation.

1. What is Polymorphism?

The word "polymorphism" comes from Greek and means "many forms." In programming, it means that the same action (a method call) can behave differently depending on the object that is performing it.

Imagine a Shape class with a draw() method. If you have different shapes like Circle, Square, and Triangle, each one will implement draw() differently. Polymorphism allows you to write code that simply calls shape.draw(), and the correct drawing behavior for the specific shape is executed automatically.

Let's start with a short video that introduces this concept using a simple, clear example.

Java Polymorphism Fully Explained In 7 Minutes

This video from 'Coding with John' will show you the basic idea of polymorphism by creating an Animal class and having subclasses like Dog and Cat provide their own unique behaviors for a common method.

Watch the video from the beginning until 05:10. Focus on how the eat() method is first inherited by the Dog class and then overridden to provide a different behavior. Notice how this is repeated for the Cat class.

As the video demonstrates, runtime polymorphism is achieved through method overriding within an inheritance structure. The Dog and Cat classes override the eat() method from the Animal superclass to provide their own specific implementations.

Runtime Polymorphism Example: Mobile OS Display Method
This diagram shows a `MobileOS` base class with a `display()` method. The `Android` and `iOS` subclasses each provide their own specific implementation of `display()`, overriding the parent's version. This is the essence of method overriding.

2. The Rules of Method Overriding in Java

For method overriding to work correctly, Java enforces a few specific rules. It's also a good practice to use the @Override annotation to make your intention clear to the compiler and other developers.

Let's review these rules and best practices.

Java Method Overriding

This article from Programiz provides a concise summary of the rules for method overriding, the purpose of the @Override annotation, how to use the super keyword, and rules regarding access modifiers.

Read the sections titled 'Example 1: Method Overriding', 'Java Overriding Rules', 'super Keyword in Java Overriding', and 'Access Specifiers in Method Overriding'. These sections cover the essential technical details you need to know.

Here's a summary of the most important points from the reading:

  • Method Signature: The overridden method in the subclass must have the same name, return type, and parameter list as the method in the superclass. (Note: The return type can be a subtype of the superclass's return type, known as a covariant return type, but for now, you can think of it as being the same).
  • @Override Annotation: While optional, it's a strong best practice. It tells the compiler you intend to override a method. If you make a mistake (e.g., misspell the method name), the compiler will give you an error, saving you from bugs.
  • final and static Methods: You cannot override methods marked as final or static. A final method is meant to be unchangeable by subclasses, and static methods belong to the class itself, not an instance.
  • Access Modifiers: The overriding method's access level cannot be more restrictive than the overridden method's. For example, if the parent method is protected, the child method can be protected or public, but not private.
  • super Keyword: Inside an overriding method, you can use super.methodName() to call the superclass's version of the method. This is useful when you want to extend the parent's behavior, not just replace it.

3. How It Works: Upcasting and Dynamic Method Dispatch

Now for the crucial question: how does Java decide which version of the method to run?

If we have this code:

Animal myPet = new Dog(); // Parent reference, child object
myPet.eat(); // Which eat() method is called?

The answer lies in two concepts: upcasting and dynamic method dispatch.

Upcasting is the process of treating a subclass object as an instance of its superclass. In the line Animal myPet = new Dog();, we are creating a Dog object but storing its reference in a variable of type Animal. This is perfectly legal because a Dog is an Animal.

Upcasting in Java
This image illustrates upcasting. We create an object of the subclass (`new B()`) and assign it to a reference variable of the superclass type (`A obj`). This is the foundation for runtime polymorphism.

When a method is called on this reference (e.g., myPet.eat()), the Java Virtual Machine (JVM) uses Dynamic Method Dispatch. At runtime, the JVM checks the actual type of the object the reference is pointing to (in this case, Dog), not the type of the reference variable (Animal). It then "dispatches" the call to the overridden method in the actual object's class.

This video explains this mechanism very clearly.

#56 Dynamic Method Dispatch in Java

The 'Telusko' channel provides an excellent explanation of Dynamic Method Dispatch, including a conceptual memory diagram that shows how the same reference variable can point to different objects at runtime, leading to different method calls.

Watch the following segments: 01:33 - 03:33: Understand how a superclass reference can hold a subclass object (upcasting). 04:32 - 06:06: Pay close attention to the memory diagram. This explains how the JVM resolves the method call at runtime by looking at the actual object. 06:06 - 06:54: See how this is extended to a third class, reinforcing the 'dynamic' nature of the dispatch.

The key takeaway is that the decision of which method to execute is made at runtime, based on the object's actual type. This is why it's called runtime polymorphism.

4. Why Polymorphism is Crucial in System Design

Understanding the mechanics is one thing, but appreciating its value is what will set you apart in a system design interview. Polymorphism allows you to write code that is flexible, loosely coupled, and extensible.

Let's explore why this is so important for building robust systems.

Polymorphism | LLD

This article from AlgoMaster connects polymorphism directly to the goals of Low-Level Design. It explains the practical benefits and shows how polymorphism is applied in interviews.

Read the sections 'Why Polymorphism Matters', 'Runtime Polymorphism (Dynamic Binding)', and 'Polymorphism in LLD Interviews'. Focus on the NotificationSender example and the benefits like loose coupling and extensibility.

In essence, polymorphism allows you to "program to an interface, not an implementation." Your code can depend on a general type (like NotificationSender or Animal) without needing to know about the specific subtypes (EmailSender, SMSSender, Dog, Cat).

This has huge benefits:

  • Flexibility & Extensibility: You can introduce new subclasses (e.g., a PushNotificationSender) with new behaviors without modifying the existing code that uses the NotificationSender interface. This directly supports the Open/Closed Principle, which we will study in a later module.
  • Loose Coupling: The client code is decoupled from the concrete implementations. It doesn't care how a notification is sent, only that it can be sent. This makes your system easier to maintain and test.
Test your understanding!

You are designing a payment processing system. You need to support payments via Credit Card, PayPal, and Bitcoin. Each payment method has a different way of processing a payment.

You have a PaymentService class that orchestrates the payment. How would you design the system using polymorphism so that PaymentService can handle any payment type without knowing the specific details of each?

Provide a simple code structure.

Show answer

A great way to design this is by using a common interface or abstract class for all payment gateways.

  1. Define a common interface:

    public interface PaymentGateway {
        void processPayment(double amount);
    }
    

    This defines the contract: any payment gateway must be able to process a payment.

  2. Create concrete implementations:

    public class CreditCardGateway implements PaymentGateway {
        @Override
        public void processPayment(double amount) {
            System.out.println("Processing credit card payment of $" + amount);
            // Add specific logic for credit card APIs
        }
    }
    
    public class PayPalGateway implements PaymentGateway {
        @Override
        public void processPayment(double amount) {
            System.out.println("Processing PayPal payment of $" + amount);
            // Add specific logic for PayPal APIs
        }
    }
    
    public class BitcoinGateway implements PaymentGateway {
        @Override
        public void processPayment(double amount) {
            System.out.println("Processing Bitcoin payment of $" + amount);
            // Add specific logic for Bitcoin transactions
        }
    }
    
  3. Use polymorphism in the client code (PaymentService):

    public class PaymentService {
        // The service doesn't know which gateway it's using, only that it's a PaymentGateway.
        public void makePayment(PaymentGateway gateway, double amount) {
            System.out.println("Initiating payment...");
            gateway.processPayment(amount); // Dynamic dispatch happens here!
            System.out.println("Payment completed.");
        }
    }
    
    // Example usage:
    public static void main(String[] args) {
        PaymentService service = new PaymentService();
        
        // At runtime, we can pass any implementation
        PaymentGateway ccGateway = new CreditCardGateway();
        service.makePayment(ccGateway, 100.0);
    
        System.out.println("---");
    
        PaymentGateway paypalGateway = new PayPalGateway();
        service.makePayment(paypalGateway, 50.0);
    }
    

    The makePayment method just works with the PaymentGateway interface. At runtime, the JVM will call the correct processPayment method based on the actual object passed (CreditCardGateway, PayPalGateway, etc.).

Conclusion

Today we took a deep dive into one of OOP's core pillars. By mastering runtime polymorphism, you can design systems that are not only functional but also clean, flexible, and ready for future expansion.

Key Takeaways:

  • Runtime Polymorphism allows an object to take "many forms," where a single method call can trigger different behaviors depending on the object's actual type.
  • It is achieved in Java via method overriding in an inheritance hierarchy.
  • The mechanism behind it is Dynamic Method Dispatch, where the JVM determines which method to execute at runtime.
  • This principle is vital for loose coupling and extensibility, allowing you to build systems that are easy to maintain and evolve—a key consideration in any system design interview.

In this lesson, we focused entirely on runtime polymorphism. However, there is another type called compile-time polymorphism. In our next lesson, we will differentiate compile-time polymorphism (method overloading) from runtime polymorphism (method overriding) to complete your understanding of this powerful concept.

Can't find a good explanation? Sign up and we'll make it for you

Sign up