Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 7

Polymorphism: Overloading vs. Overriding

Recommended resources

Welcome back! In the last two lessons, we've explored two powerful, yet distinct, features of Java: method overriding and method overloading. You learned that overriding allows a subclass to provide a specific implementation for a method inherited from its parent, while overloading allows a class to have multiple methods with the same name but different parameters.

Today, we'll bring these two concepts together to directly compare and contrast them. The goal of this lesson is to solidify your understanding of when and why to use each one. This distinction is fundamental to grasping polymorphism, a cornerstone of object-oriented programming, and is a very common topic in technical interviews for Java developers. By the end of this lesson, you will be able to clearly distinguish between compile-time (overloading) and runtime (overriding) polymorphism.

Polymorphism: The Two Flavors

Polymorphism allows us to perform a single action in different ways. In Java, this is primarily achieved through method overloading and method overriding. While they both use methods with the same name, they operate on completely different principles and at different times in a program's life cycle.

Let's start with a high-level visual comparison.

This diagram provides a side-by-side comparison of method overloading and method overriding. Notice that overloading occurs within a single class with different arguments, while overriding occurs between a parent and child class with the same arguments.

This image captures the structural difference. Now, let's explore the conceptual difference, which revolves around when the decision is made about which method to call.

The following video provides an excellent explanation of both concepts using real-life analogies—animal sounds for overriding and booking movie tickets for overloading—before diving into the code. This will help build your intuition.

Polymorphism in Java Explained with Real-Life Examples | Method Overloading & Overriding Simplified

This video from CodeSnippet clearly explains both types of polymorphism with practical examples and code implementations.

Watch the following sections: Runtime Polymorphism (Overriding): The presenter uses an Animal class with Dog and Cat subclasses to demonstrate how the speak() method is resolved at runtime. Pay close attention to the terms "dynamic binding" and "late binding." Compile-time Polymorphism (Overloading): Here, a Theater class is used to show how multiple bookTickets() methods with different parameters are resolved by the compiler. Note the terms "static binding" and "early binding."

As the video highlights, the core difference is about timing:

  • Method Overloading is resolved at compile time. The compiler analyzes the method call (myObject.myMethod(arg1, arg2)) and matches it to the correct overloaded version based on the number and type of arguments. This is static polymorphism or early binding.
  • Method Overriding is resolved at run time. The compiler only knows that you're calling a method that exists in the parent class. The Java Virtual Machine (JVM) waits until the program is running to check the actual type of the object and then calls the appropriate version of the method. This is runtime polymorphism or dynamic binding.

A Structured Comparison

To make the distinctions crystal clear, let's look at a summary table. The following article from DigitalOcean provides a concise and accurate breakdown.

Overriding vs Overloading in Java

This article offers a direct comparison that neatly summarizes the key differences.

Please read the section Comparing overriding and overloading. Focus on the table, as it directly contrasts the two concepts across several important criteria.

Let's distill the information from that table into a few key points:

Feature Method Overloading Method Overriding
Purpose To add more methods with the same name but different functionality within the same class. To provide a specific implementation of an inherited method in a subclass.
Relationship Occurs within a single class. Occurs between a superclass and subclass (inheritance).
Method Signature Name is the same, but the parameter list must be different (number, type, or order of parameters). Name and the parameter list must be the same.
Polymorphism Type Compile-time (Static) Polymorphism Runtime (Dynamic) Polymorphism
Binding Time Early Binding (at compile time) Late Binding (at run time)
Static Methods Can be overloaded. Cannot be overridden (this is called method hiding).
Access Modifiers Can have any access modifier. Cannot have a more restrictive access modifier than the parent's method.

The "How": Dynamic Method Dispatch

You might be wondering how the JVM achieves runtime polymorphism. The mechanism behind it is called Dynamic Method Dispatch. This is the process that allows a superclass reference variable to refer to a subclass object and still invoke the subclass's overridden method.

Consider this classic example:

class Animal {
    public void makeSound() {
        System.out.println("Generic animal sound");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Woof");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Dog(); // Superclass reference, subclass object
        myAnimal.makeSound(); // Which method is called?
    }
}

The variable myAnimal is of type Animal, but the object it points to is of type Dog. At compile time, the compiler only checks if the Animal class has a makeSound() method. It does, so the code compiles. However, at runtime, the JVM inspects the actual object (new Dog()) and sees that it has an overridden makeSound() method. Therefore, it calls the Dog's version, and the output is "Woof". This decision-making process at runtime is Dynamic Method Dispatch.

The following video from Telusko provides a fantastic walkthrough of this exact concept.

#56 Dynamic Method Dispatch in Java

This video explains the mechanics of runtime polymorphism through Dynamic Method Dispatch.

Watch from the main explanation. The presenter does an excellent job of: Showing how a parent reference can hold a child object (A obj = new B()). Demonstrating that the overridden method in the child class is called. Using a memory diagram to illustrate how the object reference points to different objects in the heap, leading to different method calls. Emphasizing that this is only possible through inheritance.


Seeing Both in Action

To tie everything together, let's examine a single piece of code that uses both overloading and overriding. The DigitalOcean article we looked at earlier provides a great example.

Overriding vs Overloading in Java

This article contains a unified code example demonstrating both concepts.

Read the section "Overriding and overloading example". In the Processor class, notice how the process method is overloaded with different parameters ((int, int), (int[]), (Object[])). Then, see how the MathProcessor subclass overrides two of these methods to provide new functionality (calculating a sum instead of just printing).

This example clearly shows the two concepts coexisting. Overloading provides different ways to call the process method, while overriding allows a specialized subclass like MathProcessor to change what those methods actually do.


Your Turn: A Notification System

Let's solidify your understanding with a practical exercise. You'll build a simple notification system that uses both overloading and overriding.

Task:

  1. Create a base class named NotificationService.

    • It should have a method: public void send(String message). This method should print [NOTIFICATION] followed by the message.
    • Overload this method by adding another version: public void send(String subject, String message). This method should print [NOTIFICATION] Subject: <subject> - Body: <message>.
  2. Create a subclass named EmailService that extends NotificationService.

    • Override the send(String message) method to print [EMAIL] Sent: followed by the message.
    • Override the send(String subject, String message) method to print [EMAIL] Sent with Subject: <subject> - Body: <message>.
  3. Create another subclass named SMSService that extends NotificationService.

    • Override the send(String message) method to print [SMS] Delivered: followed by the message.
    • Override the send(String subject, String message) method. For an SMS, a subject doesn't make much sense, so have it combine them: [SMS] Delivered: <subject> - <message>.
  4. Create a Main class to test your system.

    • Create an EmailService object and an SMSService object.
    • Use a NotificationService reference variable for each (upcasting):
      NotificationService emailSender = new EmailService();
      NotificationService smsSender = new SMSService();
      
    • Call both the single-parameter and two-parameter send methods on emailSender and smsSender.
    • Observe how the JVM uses runtime polymorphism to call the correct overridden methods from EmailService and SMSService, even though the reference type is NotificationService.

Summary

Today we dissected the two types of polymorphism in Java, a concept crucial for building flexible and maintainable object-oriented systems.

Key Takeaways:

  • Polymorphism means "many forms"; it's the ability to perform an action in different ways.
  • Method Overloading is compile-time polymorphism. It occurs within a single class, using the same method name with different parameter lists. The compiler chooses the method based on the arguments.
  • Method Overriding is runtime polymorphism. It occurs in a parent-child class relationship, where the child provides a specific implementation for an inherited method. The JVM chooses the method at runtime based on the actual object's type, via Dynamic Method Dispatch.
  • Overloading is about providing more options or flexibility (add(int, int), add(int, int, int)), while overriding is about changing behavior (Animal.speak() vs. Dog.speak()).

In our next lesson, we'll build directly on the idea of NotificationService sender = new EmailService();. We will formally explore upcasting, its counterpart downcasting, and how to safely check an object's type at runtime using the instanceof operator.