Java Polymorphism


Polymorphism is one of the core concepts in object-oriented programming (OOP), and Java fully supports it. The term polymorphism comes from the Greek words “poly” (meaning many) and “morph” (meaning form). In Java, polymorphism allows objects to be treated as instances of their parent class, even though they may be instances of derived classes. This allows one interface to be used for a general class of actions, making your code more flexible and extensible.

In this guide, we will explore the concept of polymorphism in Java, its types, benefits, and examples.

Table of Contents

  1. What is Polymorphism?
  2. Types of Polymorphism in Java
  3. How Polymorphism Works in Java
  4. Advantages of Polymorphism
  5. Polymorphism and Method Overloading
  6. Polymorphism and Method Overriding
  7. Example of Polymorphism in Java
  8. Common Pitfalls of Polymorphism

What is Polymorphism?

Polymorphism allows a single action to behave differently based on the object it is acting upon. In Java, polymorphism means that the same method or operator can perform different tasks. It enables objects of different types to be treated as objects of a common supertype.

Key Features of Polymorphism in Java:

  • Method Overloading: Multiple methods with the same name, but with different parameters.
  • Method Overriding: A subclass provides a specific implementation for a method that is already defined in its superclass.
  • Dynamic Method Dispatch: This is the process of calling a method based on the object type at runtime.

Types of Polymorphism in Java

In Java, polymorphism can be divided into two major types:

2.1 Compile-time Polymorphism (Method Overloading)

Compile-time polymorphism, also known as method overloading, occurs when multiple methods have the same name but differ in the number or type of parameters. The correct method is determined at compile time based on the method signature.

Example of Method Overloading:

class Calculator {
    // Method to add two integers
    int add(int a, int b) {
        return a + b;
    }

    // Overloaded method to add three integers
    int add(int a, int b, int c) {
        return a + b + c;
    }

    // Overloaded method to add two doubles
    double add(double a, double b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println("Sum of 2 integers: " + calc.add(5, 10));           // Output: 15
        System.out.println("Sum of 3 integers: " + calc.add(5, 10, 15));       // Output: 30
        System.out.println("Sum of 2 doubles: " + calc.add(5.5, 10.5));       // Output: 16.0
    }
}

In this example, the add method is overloaded with different parameter types and counts. The appropriate method is called based on the parameters passed.

2.2 Runtime Polymorphism (Method Overriding)

Runtime polymorphism, also known as method overriding, occurs when a subclass provides its own implementation for a method that is already defined in its superclass. The method that gets called is determined at runtime, depending on the object being referenced.

Example of Method Overriding:

class Animal {
    // Method in superclass
    void sound() {
        System.out.println("Animal makes a sound");
    }
}

class Dog extends Animal {
    // Overriding method in subclass
    @Override
    void sound() {
        System.out.println("Dog barks");
    }
}

class Main {
    public static void main(String[] args) {
        Animal animal = new Animal(); // Creating an Animal object
        animal.sound();  // Output: Animal makes a sound

        Animal dog = new Dog();   // Creating a Dog object using Animal reference
        dog.sound();  // Output: Dog barks (Method overriding in action)
    }
}

In this example, the sound() method is defined in both the Animal class and overridden in the Dog class. The correct method is chosen at runtime based on the object type (whether animal is an Animal or Dog).


How Polymorphism Works in Java

In Java, dynamic method dispatch is the mechanism that determines which method to invoke based on the actual object at runtime, not the reference type. This is primarily related to method overriding, where the method to call is determined by the object that is assigned to the reference.

In the Main class, the variable dog is of type Animal, but it holds an instance of Dog. At runtime, the overridden method in the Dog class is invoked, demonstrating polymorphism.


Advantages of Polymorphism

  1. Code Reusability: Polymorphism allows classes to share common behavior while implementing their own unique behaviors, reducing code duplication.
  2. Flexibility and Extensibility: New classes can be introduced without changing the existing code. Polymorphism allows systems to be more flexible and adaptable.
  3. Simplified Code Maintenance: Polymorphism allows changes to be made to the methods or behaviors of objects in a consistent and centralized way, making maintenance easier.

Polymorphism and Method Overloading

Method overloading is a common example of compile-time polymorphism. It occurs when there are multiple methods with the same name but different signatures. The method that will be called is resolved during the compilation based on the parameters passed.

Example of Method Overloading:

class Printer {
    void print(String message) {
        System.out.println("String message: " + message);
    }

    void print(int number) {
        System.out.println("Integer message: " + number);
    }
}

public class Main {
    public static void main(String[] args) {
        Printer printer = new Printer();
        printer.print("Hello, World!");  // Output: String message: Hello, World!
        printer.print(123);              // Output: Integer message: 123
    }
}

In this example, the print method is overloaded with different parameter types (String and int). The correct method is selected at compile-time based on the argument passed.


Polymorphism and Method Overriding

Method overriding is a key aspect of runtime polymorphism. It occurs when a subclass provides a specific implementation for a method that is already defined in its superclass. The overridden method is called at runtime based on the object type.

Example of Method Overriding:

class Vehicle {
    void drive() {
        System.out.println("Vehicle is driving");
    }
}

class Car extends Vehicle {
    @Override
    void drive() {
        System.out.println("Car is driving");
    }
}

class Main {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle();
        vehicle.drive();   // Output: Vehicle is driving

        Vehicle car = new Car();
        car.drive();   // Output: Car is driving (Method overriding in action)
    }
}

In this case, the drive() method is overridden in the Car class. At runtime, the correct method is called depending on the object type, demonstrating polymorphism.


Common Pitfalls of Polymorphism

  1. Confusion Between Overloading and Overriding: Method overloading is resolved at compile-time, while method overriding is resolved at runtime. It’s important to distinguish between these two concepts.
  2. Method Signature Mismatch: When overriding a method, the method signature in the subclass must exactly match the one in the superclass. Any difference will result in a compilation error.
  3. Casting Issues: When using polymorphism, improper casting can lead to runtime errors (e.g., ClassCastException). Always ensure that the object being cast is compatible with the target type.