OOP

Polymorphism in Java Tutorial with Code Examples

Polymorphism is one of the four pillars of object-oriented programming (OOP), along with encapsulation, inheritance, and abstraction.

Polymorphism allows objects of different classes to be treated as objects of a common super class. It enables a single method or interface to perform different functions based on the object it is acting upon.

In simpler terms, polymorphism allows methods to be used interchangeably for objects of different types.

This tutorial will explain polymorphism in Java, focusing on two main types:

Compile-time (Static) Polymorphism using method overloading.
Runtime (Dynamic) Polymorphism using method overriding.
We'll explore these concepts with several examples to help you understand how polymorphism works in Java.

Table of Contents:

1. What is Polymorphism?

Polymorphism allows objects of different classes to respond to the same method call in different ways. The word “polymorphism” means “many shapes,” and in Java, polymorphism enables the same method to behave differently depending on the object that invokes it.

There are two types of polymorphism in Java:

Compile-time polymorphism (Method Overloading): Occurs when multiple methods with the same name but different parameters are defined in a class.
Runtime polymorphism (Method Overriding): Occurs when a subclass provides a specific implementation for a method that is already defined in its parent class.

2. Compile-Time (Static) Polymorphism

Method Overloading

Method overloading occurs when multiple methods with the same name are defined in a class but differ in:

The number of parameters.
The types of parameters.
The order of parameters.

Example 1: Method Overloading

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

    // Method to add three integers (overloaded)
    public int add(int a, int b, int c) {
        return a + b + c;
    }

    // Method to add two doubles (overloaded)
    public double add(double a, double b) {
        return a + b;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();

        // Calls the overloaded methods
        System.out.println("Sum of two integers: " + calc.add(5, 3));      // Output: 8
        System.out.println("Sum of three integers: " + calc.add(5, 3, 2)); // Output: 10
        System.out.println("Sum of two doubles: " + calc.add(5.5, 3.2));   // Output: 8.7
    }
}

Explanation:

The add method is overloaded to accept different numbers and types of arguments. The correct method is chosen at compile time based on the method signature.

3. Runtime (Dynamic) Polymorphism

Method Overriding

Method overriding occurs when a subclass provides a specific implementation for a method that is already defined in its superclass.

This is where polymorphism really shines—when an object of the subclass is referred to by a superclass reference, the overridden method in the subclass is called at runtime.

Example 2: Method Overriding

class Animal {
    // Method that can be overridden
    public void sound() {
        System.out.println("Animal makes a sound");
    }
}

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

class Cat extends Animal {
    // Overriding the sound method
    @Override
    public void sound() {
        System.out.println("Cat meows");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Animal();  // Create an Animal object
        Animal myDog = new Dog();        // Create a Dog object
        Animal myCat = new Cat();        // Create a Cat object

        myAnimal.sound();  // Output: Animal makes a sound
        myDog.sound();     // Output: Dog barks
        myCat.sound();     // Output: Cat meows
    }
}

Explanation:

The method sound() is overridden in the Dog and Cat classes. When the method is called on objects of these subclasses, their respective implementations are invoked at runtime, even though they are referenced by a Animal type variable.

4. Upcasting and Downcasting

Upcasting

Upcasting refers to treating an object of a subclass as an object of its superclass. This is useful for achieving polymorphism.

Example 3: Upcasting

class Animal {
    public void sound() {
        System.out.println("Animal makes a sound");
    }
}

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

public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog();  // Upcasting
        myDog.sound();  // Output: Dog barks (polymorphism in action)
    }
}

Explanation:

The myDog object is of type Dog, but it is upcasted to Animal. The overridden sound() method of the Dog class is called due to polymorphism.

Downcasting

Downcasting refers to casting a reference of a superclass type to a subclass type. This must be done explicitly, and you should ensure the actual object is an instance of the subclass.

Example 4: Downcasting

public class Main {
    public static void main(String[] args) {
        Animal myAnimal = new Dog();  // Upcasting
        if (myAnimal instanceof Dog) {
            Dog myDog = (Dog) myAnimal;  // Downcasting
            myDog.sound();  // Output: Dog barks
        }
    }
}

Explanation:

The instanceof keyword ensures that the object myAnimal is actually an instance of the Dog class before downcasting.

5. Polymorphism with Interfaces

In addition to classes, polymorphism can be achieved using interfaces. When multiple classes implement the same interface, objects of those classes can be referenced by the interface type, allowing for polymorphic behavior.

Example 5: Polymorphism with Interfaces

interface Vehicle {
    void start();
}

class Car implements Vehicle {
    @Override
    public void start() {
        System.out.println("Car starts");
    }
}

class Bike implements Vehicle {
    @Override
    public void start() {
        System.out.println("Bike starts");
    }
}

public class Main {
    public static void main(String[] args) {
        Vehicle myCar = new Car();  // Polymorphism using an interface
        Vehicle myBike = new Bike();

        myCar.start();  // Output: Car starts
        myBike.start(); // Output: Bike starts
    }
}

Explanation:

Both Car and Bike implement the Vehicle interface. The start() method behaves differently depending on which object it is invoked on.

6. Best Practices and Use Cases for Polymorphism

Code Reusability: Polymorphism allows you to write flexible and reusable code by using common interfaces or parent classes, which can be extended or implemented by multiple classes.

Decoupling Code: It decouples code by relying on abstract types (such as interfaces or parent classes), rather than specific classes. This makes your code more maintainable and extensible.

Simplified Code: Polymorphism simplifies code by allowing you to call the same method on different types of objects without having to check their type explicitly.

7. Conclusion

Polymorphism is a powerful concept in Java that allows objects of different types to respond to the same method call in their own way.

Through method overloading (compile-time polymorphism) and method overriding (runtime polymorphism), Java enables developers to write flexible and reusable code. Whether through class inheritance or interface implementation, polymorphism ensures that your programs are easier to maintain, extend, and debug.

By mastering polymorphism, you gain a deeper understanding of object-oriented programming and how to make your code more dynamic and versatile in real-world applications.

Related posts

Java Hidden Classes

Java sealed classes

Java Static Classes