Polymorphism in Java Explained [In-Depth Tutorial]


JAVA

Author: Bashir Alam
Reviewer: Deepak Prasad

Polymorphism is a fundamental concept in object-oriented programming that allows objects of different classes to be treated as if they were the same type of object. In Java, polymorphism can be implemented using inheritance, interfaces, and method overloading. In this article, we will explore what polymorphism is, how it works, and how it can be implemented in Java.

 

What is Polymorphism in Java?

A key idea in object-oriented programming (OOP) is polymorphism, which enables objects from various classes to be handled as though they were of the same type. Java uses inheritance, interfaces, and method overloading to implement polymorphism.

The advantage of polymorphism is that it allows you to write code that can work with objects of different classes, without having to know the specific class of the object at compile time. This makes your code more flexible and extensible. It also allows you to write code that is easier to maintain, as changes to one class will not affect the behavior of other classes that use it.

 

Types of Polymorphism in Java

There are two types of polymorphism in Java: compile-time polymorphism and runtime polymorphism.

Polymorphism-in-java

 

Compile Time Polymorphism

Compile-time polymorphism is achieved through method overloading. Method overloading allows multiple methods to have the same name but with different parameters. When a method is called, the Java compiler determines which method to call based on the number and type of arguments passed to it at compile time. This allows you to create methods with the same name that perform different tasks based on the input.

Here's an example to illustrate method overloading:

public class Calculator {
    public int add(int x, int y) {
        return x + y;
    }
    
    public double add(double x, double y) {
        return x + y;
    }
}

In this example, we have a Calculator class with two add() methods. One method takes two integers as input, while the other takes two doubles. When the add() method is called, the Java compiler determines which method to call based on the input.

 

Runtime Polymorphism

Runtime polymorphism is achieved through inheritance and interfaces. Inheritance allows a subclass to inherit the properties and methods of its parent class. By creating a subclass that inherits from a parent class, you can use the subclass object to access the properties and methods of both the subclass and the parent class.

Interfaces allow you to define a set of methods that a class must implement, but they do not provide any implementation for those methods. A class can implement multiple interfaces, allowing it to inherit the methods of multiple interfaces.

Here's an example to illustrate runtime polymorphism:

public interface Animal {
    public void makeSound();
}

public class Dog implements Animal {
    public void makeSound() {
        System.out.println("The dog barks");
    }
}

public class Cat implements Animal {
    public void makeSound() {
        System.out.println("The cat meows");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal1 = new Dog();
        Animal animal2 = new Cat();
        animal1.makeSound();
        animal2.makeSound();
    }
}

In this example, we have an Animal interface with a method called makeSound(), and two classes called Dog and Cat that implement the Animal interface and override the makeSound() method. We then create two objects, animal1 and animal2, of the Animal type, but assign them to instances of Dog and Cat, respectively. Finally, we call the makeSound() method on both objects. Because of polymorphism, the makeSound() method is called on the Dog and Cat objects, even though the Animal type does not have a specific makeSound() method implementation.

The output of the program will be:

The dog barks
The cat meows

As you can see, even though animal1 and animal2 are of the Animal type, they call the makeSound() method of their respective subclass at runtime, demonstrating the power of polymorphism

 

Examples of Polymorphism in Java

To understand the concept better, we will go through various examples in this section.

 

Example-1: Method Overloading

Method overloading is a form of polymorphism where multiple methods in a class have the same name but different parameters. The Java compiler determines which method to call based on the number and types of arguments passed to the method. Here's an example:

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

In the above code, the Calculator class has two methods with the same name add, but one takes two int arguments and the other takes two double arguments. The return types are different as well. When the add method is called, the Java compiler determines which version of the method to call based on the argument types.

Calculator calculator = new Calculator();
int sum1 = calculator.add(1, 2);
double sum2 = calculator.add(1.5, 2.5);

Output:

3
4.0

In the above code, the first call to add will call the add(int, int) method and return an int value of 3. The second call to add will call the add(double, double) method and return a double value of 4.0.

 

Example-2: Method Overriding

Method overriding is a form of runtime polymorphism, where a subclass provides a new implementation for a method inherited from its superclass. This allows the subclass to inherit methods and fields from the superclass, while still being able to customize the behavior when needed. Here's an example:

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

In the above code, the Dog class extends the Animal class and overrides the makeSound method. When the makeSound method is called on a Dog object, the Dog class's implementation of the method will be called.

Animal animal = new Animal();
Dog dog = new Dog();
animal.makeSound();
dog.makeSound();   

Output:

Animal is making a sound
Woof!

In the above code, the first call to makeSound will call the makeSound method in the Animal class and output "Animal is making a sound". The second call to makeSound will call the makeSound method in the Dog class and output "Woof!".

 

Example-3: Polymorphic References

Polymorphic references allow objects of different classes to be treated as objects of the same superclass. This can be useful in cases where you want to create a collection of objects of different types that share a common superclass. Here's an example:

public class Animal {
   public void makeSound() {
      System.out.println("Animal is making a sound");
   }
}
public class Dog extends Animal {
   @Override
   public void makeSound() {
      System.out.println("Woof!");
   }
}
public class Cat extends Animal {
   @Override
   public void makeSound() {
      System.out.println("Meow!");
   }
}

In the above code, the Animal, Dog, and Cat classes are defined as before. Now let's create an array of Animal objects that contains instances of Animal, Dog, and Cat:

Animal[] animals = new Animal[3];
animals[0] = new Animal();
animals[1] = new Dog();
animals[2] = new Cat();

for (Animal animal : animals) {
   animal.makeSound();
}

Output:

Animal is making a sound
Woof!
Meow!

In the above code, we create an array of Animal objects and assign them to instances of Animal, Dog, and Cat. Then we loop through the array and call the makeSound method on each object. Because the makeSound method is overridden in the Dog and Cat classes, the appropriate version of the method will be called for each object.

 

Example-4: Dynamic Method Dispatch

Dynamic method dispatch refers to the process by which the Java Virtual Machine (JVM) determines which method implementation to call at runtime, based on the actual object type. This enables objects of different classes to be treated as objects of a common superclass, while still calling the appropriate method implementation.

Shape shape1 = new Shape();
Shape shape2 = new Circle();

shape1.draw(); // Output: Drawing a shape
shape2.draw(); // Output: Drawing a circle

 

Abstract Classes and Interfaces

Abstract classes and interfaces are mechanisms that enforce polymorphism in Java. An abstract class can contain abstract methods, which have no implementation in the abstract class and must be implemented by any concrete (non-abstract) subclass. Interfaces define a contract (a set of method signatures) that implementing classes must adhere to.

abstract class Animal {
    abstract void makeSound();
}

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

interface Drawable {
    void draw();
}

class Rectangle implements Drawable {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}
class Square implements Drawable {
    @Override
    public void draw() {
        System.out.println("Drawing a square");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.makeSound(); // Output: Woof!

        Drawable[] drawables = new Drawable[2];
        drawables[0] = new Rectangle();
        drawables[1] = new Square();

        for (Drawable drawable : drawables) {
            drawable.draw();
        }
        // Output:
        // Drawing a rectangle
        // Drawing a square
    }
}

In this example, we have an abstract class Animal with an abstract method makeSound(). The Dog class extends Animal and provides an implementation for makeSound().

We also have an interface Drawable, which defines a draw() method. Two classes, Rectangle and Square, implement the Drawable interface and provide their own implementation for the draw() method.

In the main method, we create an Animal object (with the actual type being Dog) and call its makeSound() method. We also create an array of Drawable objects and iterate through them, calling the draw() method on each object. This demonstrates how abstract classes and interfaces can be used to enforce polymorphism in Java, allowing objects of different classes to be treated as objects of a common superclass or interface.

 

Summary

Polymorphism is a core concept in Java's object-oriented programming, enabling objects of different classes to be treated as objects of a common superclass or interface. It promotes flexibility, code reusability, and maintainability. There are two types of polymorphism in Java: compile-time and runtime polymorphism. Compile-time polymorphism is achieved through method overloading, where multiple methods with the same name but different parameters exist within a class. Runtime polymorphism is achieved through method overriding, where a subclass provides a new implementation for a method inherited from its superclass. The JVM uses dynamic method dispatch to determine which method implementation to call at runtime, based on the actual object type. Abstract classes and interfaces further enforce polymorphism in Java. Abstract classes can contain abstract methods that must be implemented by any concrete subclass, while interfaces define a contract that implementing classes must adhere to. By understanding and applying polymorphism, developers can create more modular, reusable, and maintainable code in Java applications.

 

Further readings

Polymorphism in Java

 

Bashir Alam

Bashir Alam

He is a Computer Science graduate from the University of Central Asia, currently employed as a full-time Machine Learning Engineer at uExel. His expertise lies in Python, Java, Machine Learning, OCR, text extraction, data preprocessing, and predictive models. You can connect with him on his LinkedIn profile.

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!

Leave a Comment