Polymorphism in Java

Java Polymorphism: A Comprehensive Guide with Examples and Exercises

Polymorphism is a core concept of object-oriented programming (OOP) that allows methods or functions to behave differently based on the object that calls them. In Java, polymorphism enables you to define one interface or method and have multiple implementations for it. This guide will cover the basics of polymorphism in Java, its types, and practical examples with exercises.


Table of Contents

  1. What is Polymorphism?
  2. Types of Polymorphism in Java
  3. Why Use Polymorphism?
  4. Method Overloading (Compile-Time Polymorphism)
  5. Method Overriding (Runtime Polymorphism)
  6. Polymorphism with Interfaces and Abstract Classes
  7. Exercises

1. What is Polymorphism?

Polymorphism is derived from two Greek words: poly (many) and morph (forms). In Java, polymorphism allows methods or objects to process differently depending on their data types or classes. The main advantage is that it enables you to perform a single action in different ways, thus making your code more flexible and easier to manage.

In simpler terms:

  • Compile-time Polymorphism is achieved through method overloading.
  • Runtime Polymorphism is achieved through method overriding.

2. Types of Polymorphism in Java

There are two types of polymorphism in Java:

  • Compile-Time Polymorphism (also known as Static Polymorphism)
  • Runtime Polymorphism (also known as Dynamic Polymorphism)

Let’s look at each type in detail.

3. Why Use Polymorphism?

Polymorphism makes code more modular, readable, and easy to maintain. It lets us define a single interface that can have multiple implementations, thereby simplifying complex hierarchies in object-oriented design. Polymorphism also facilitates code reusability and flexibility, which can be crucial for large applications.

4. Method Overloading (Compile-Time Polymorphism)

Method Overloading occurs when multiple methods in the same class have the same name but different parameter lists (type, number, or order). The Java compiler determines which method to call based on the method signature.

Example of Method Overloading

class Adder {
    public int add(int a, int b) {
        return a + b;
    }

    public double add(double a, double b) {
        return a + b;
    }

    public int add(int a, int b, int c) {
        return a + b + c;
    }
}

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

        System.out.println(adder.add(5, 10));          // Calls add(int, int)
        System.out.println(adder.add(5.5, 3.2));       // Calls add(double, double)
        System.out.println(adder.add(1, 2, 3));        // Calls add(int, int, int)
    }
}

Explanation

In the Adder class, the add method is overloaded to accept different parameters. Based on the arguments passed, the compiler selects the appropriate method during compile-time.

5. Method Overriding (Runtime Polymorphism)

Method Overriding occurs when a subclass provides a specific implementation of a method already defined in its superclass. Here, Java uses dynamic binding, where the method to call is determined at runtime based on the object type.

Example of Method Overriding

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");
    }
}

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

public class Main {
    public static void main(String[] args) {
        Animal myDog = new Dog();   // Upcasting
        Animal myCat = new Cat();   // Upcasting

        myDog.sound();  // Outputs "Dog barks"
        myCat.sound();  // Outputs "Cat meows"
    }
}

Explanation

In this example, Dog and Cat override the sound method of the Animal superclass. At runtime, the Java Virtual Machine (JVM) determines which version of the sound method to call, based on the object type (i.e., Dog or Cat).

6. Polymorphism with Interfaces and Abstract Classes

Polymorphism is also applied using interfaces and abstract classes. Since interfaces and abstract classes can define methods without implementing them, subclasses or implementing classes can define these methods according to their specific needs.

Example with Interface

interface Shape {
    void draw();
}

class Circle implements Shape {
    public void draw() {
        System.out.println("Drawing a Circle");
    }
}

class Rectangle implements Shape {
    public void draw() {
        System.out.println("Drawing a Rectangle");
    }
}

public class Main {
    public static void main(String[] args) {
        Shape circle = new Circle();
        Shape rectangle = new Rectangle();

        circle.draw();         // Outputs "Drawing a Circle"
        rectangle.draw();      // Outputs "Drawing a Rectangle"
    }
}

Explanation

In this example, Circle and Rectangle implement the Shape interface and provide specific implementations of the draw method. This allows polymorphic behavior by treating circle and rectangle as Shape objects, even though they are actually Circle and Rectangle instances.

7. Exercises

Exercise 1: Compile-Time Polymorphism (Method Overloading)

Create a class Calculator with overloaded methods multiply that:

  1. Accepts two integer values and returns their product.
  2. Accepts two double values and returns their product.
  3. Accepts three integer values and returns their product.

Write a main method to test each multiply method.

Exercise 2: Runtime Polymorphism (Method Overriding)

Create a superclass Vehicle with a method move(), which prints a generic message. Then, create two subclasses Car and Bike that override move() to print messages specific to each vehicle type.

Write a main method where:

  1. You create an array of Vehicle references containing instances of Car and Bike.
  2. Loop through the array and call the move() method on each Vehicle to see polymorphism in action.

Exercise 3: Polymorphism with Interfaces

Define an interface Playable with a method play(). Create two classes Piano and Guitar that implement Playable and provide specific implementations for the play method.

In your main method:

  1. Create instances of Piano and Guitar.
  2. Store them in an array of Playable references and call the play() method on each element in the array.

Sample Solution for Exercise 1

class Calculator {
    public int multiply(int a, int b) {
        return a * b;
    }

    public double multiply(double a, double b) {
        return a * b;
    }

    public int multiply(int a, int b, int c) {
        return a * b * c;
    }
}

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

        System.out.println(calc.multiply(4, 5));             // Uses int multiply(int, int)
        System.out.println(calc.multiply(2.5, 3.5));         // Uses double multiply(double, double)
        System.out.println(calc.multiply(2, 3, 4));          // Uses int multiply(int, int, int)
    }
}

Conclusion

Polymorphism is a powerful concept in Java that brings flexibility and scalability to your code. By mastering polymorphism, you can create more adaptable and reusable code structures. With method overloading and overriding, you have two primary ways to achieve polymorphism. Using exercises, you can deepen your understanding and ability to apply polymorphism effectively. Happy coding!

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top