In object-oriented programming, you’re likely to face scenarios where you need to perform different actions based on the type of object you’re working with.
This is where polymorphism comes in – a fundamental concept in Java that allows you to write code that can work with different classes and objects seamlessly.
In this tutorial, you’ll learn how to harness the power of polymorphism in Java to create more flexible, reusable, and maintainable code.
Through practical examples and real-world scenarios, you’ll discover how to apply polymorphism to write more efficient and effective Java programs.

What is Polymorphism in Java?

Polymorphism is a fundamental concept in object-oriented programming and since java is an OOP language, polymorphism applies here as well.
Polymorphism allows you to write more efficient, flexible, and reusable code.

At its core, polymorphism is the ability of an object to take on multiple forms.
In Java, this means that an object of a particular class can behave like an object of a different class.
This is achieved through method overriding or method overloading, which allows objects of different classes to respond to the same method call.
As a result, you can write code that can work with objects of different classes without knowing their specific class type at compile time.

To illustrate this concept, consider a real-world scenario where you have a Vehicle class with subclasses like Car, Truck, and Motorcycle.
Each of these subclasses has its own implementation of the start() method.
Using polymorphism, you can create a method that takes a vehicle object as a parameter and calls the start() method, without knowing the specific type of vehicle.
This way, you can write a single method that can work with any type of vehicle, making your code more flexible and reusable.

Polymorphism Example

Here’s an example of polymorphism in action

public abstract class Vehicle { 
  public abstract void start(); 
} 

public class Car extends Vehicle { 
  public void start() { 
    System.out.println("Starting the car..."); 
  } 
} 

public class Truck extends Vehicle { 
  public void start() { 
    System.out.println("Starting the truck..."); 
  } 
} 

public class Main { 
  public static void startVehicle(Vehicle vehicle) { 
    vehicle.start(); 
  } 

  public static void main(String[] args) { 
    Car car = new Car(); 
    Truck truck = new Truck(); 
    startVehicle(car); 
    startVehicle(truck);
  } 
}

In this example, startVehicle() method takes a Vehicle object as a parameter and calls the start() method.
Because of polymorphism, you can pass objects of different subclasses (Car and Truck) to this method, and it will call the correct implementation of the start() method based on the object’s actual class type.

Below is the output

Starting the car…
Starting the truck…

Types of Polymorphism in Java

Polymorphism in Java is further divided into two main categories:
A. Runtime polymorphism, and
B. Compile-time polymorphism.

Below are their meanings

  • Runtime polymorphism, also known as dynamic method dispatch, occurs when the decision about which method to call is made at runtime.
  • Compile-time polymorphism, also known as method overloading, occurs when the decision about which method to call is made at compile time.

To better understand the differences between these two types of polymorphism, let’s take a look at the following table:

Type of PolymorphismDescription
Runtime Polymorphism (Dynamic Method Dispatch)Method overriding, where the method to be called is determined at runtime based on the object’s class.
Example: A subclass provides a specific implementation of a method already defined in its superclass.
Compile-time Polymorphism (Method Overloading)Method overloading, where the method to be called is determined at compile time based on the method signature.
Example: A class has multiple methods with the same name but different parameters.

Method Overloading in Java Polymorphism

In Java, method overloading is a type of compile-time polymorphism that allows you to define multiple methods with the same name but different parameters.
This means you can have multiple methods with the same name, but each method has a unique signature, which is a combination of the method name and its parameters.

Let’s take a look at an example to illustrate this concept: java

public class Calculator { 
  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; 
  } 
}

In this example, we have a Calculator class with three add() methods.
Each method has the same name, but they differ in their parameters.
The first method takes two int parameters, the second method takes two double parameters, and the third method takes three int parameters.

When you call the add() method, Java will determine which method to use based on the number and types of arguments you pass.
This is an example of method overloading, where the same method name is used to perform different operations based on the input parameters.

Now, you might wonder how method overloading achieves polymorphism in Java.
The answer lies in the fact that method overloading allows you to write code that can work with different data types and parameters without having to create separate methods for each scenario.
This flexibility enables you to write more generic and reusable code, which is a key principle of polymorphism.

Method Overriding in Java Polymorphism

Unlike method overloading, which allows you to define multiple methods with the same name but different parameters, method overriding is a concept where you provide a specific implementation for a method that is already defined in its superclass.

In other words, when you override a method, you are providing a new implementation for a method that is inherited from its parent class.
This allows you to customize the behavior of the method for your specific subclass.

Here’s an example to illustrate method overriding

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

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

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

public class Main { 
  public static void main(String[] args) { 
    Animal myAnimal = new Animal(); 
    Animal myDog = new Dog(); 
    Animal myCat = new Cat(); 
    myAnimal.sound(); // Output: The animal makes a sound 
    myDog.sound(); // Output: The dog barks 
    myCat.sound(); // Output: The cat meows 
  } 
}

In this example, we have an Animal class with a sound() method.
We then create two subclasses, Dog and Cat, which override the sound() method to provide their own implementation.
When we create objects of these classes and call the sound() method, the overridden method is called, resulting in different outputs.

Below is the output

The animal makes a sound
The dog barks
The cat meows

As you can see, method overriding allows you to achieve polymorphism by providing a customized implementation for a method that is inherited from its parent class.
This enables you to write more flexible and reusable code.

Java OOP Polymorphism

To fully understand how polymorphism works in Java, it’s important to understand its relationship with inheritance.
In object-oriented programming (OOP), inheritance is a mechanism that allows one class to inherit the properties and behavior of another class.
When you create a subclass that inherits from a superclass, you can override the methods of the superclass to provide a specific implementation.

In Java, polymorphism is achieved through inheritance, where a subclass provides a specific implementation of a method that is already defined in its superclass.
This allows you to treat objects of different classes as if they were of the same class, as long as they share a common superclass.

By using inheritance and method overriding, you can achieve polymorphism in Java, which allows you to write more flexible and reusable code.

Java Abstract Classes and Polymorphism

In this section, we will explore the role of abstract classes in achieving polymorphism in Java.

An abstract class is a class that cannot be instantiated on its own and is designed to be inherited by other classes.
Abstract classes provide a way to define a blueprint for other classes to follow, while also allowing for some implementation details to be shared among subclasses.

As far as polymorphism, abstract classes also play an important role.
By using abstract classes, you can define a common interface or behavior that can be shared among multiple subclasses.
This enables you to write code that can work with objects of different classes, without knowing their specific type at compile-time.

Let’s consider an example to illustrate this concept.
Suppose you’re building a simulation of a zoo, and you want to model different types of animals.
You can create an abstract class Animal that provides a common interface for all animals, such as sound() and eat().
Then, you can create concrete subclasses like Lion, Elephant, and Monkey that inherit from Animal and provide their own implementation of these methods.

public abstract class Animal { 
  public abstract void sound(); 

  public abstract void eat(); 
} 

public class Lion extends Animal { 
  @Override 
  public void sound() { 
    System.out.println("Roar!"); 
  } 
  
  @Override 
  public void eat() { 
    System.out.println("Eating meat..."); 
  } 
} 

public class Elephant extends Animal { 
  @Override 
  public void sound() { 
    System.out.println("Trumpet!"); 
  } 

  @Override 
  public void eat() { 
    System.out.println("Eating plants..."); 
  } 
}

In this example, you can create a list of Animal objects and add instances of Lion and Elephant to it.
Then, you can iterate over the list and call the sound() and eat() methods on each object, without knowing its specific type at compile-time.
This is an example of polymorphism.

By using abstract classes, we can achieve polymorphism because we define a common interface that can be shared among multiple subclasses.
This allows us to write code that can work with objects of different classes, without knowing their specific type at compile-time.

Java Interfaces and Polymorphism

In the previous sections, we learned how inheritance and abstract classes help achieve polymorphism.
Now, let’s look at the role of interfaces in enabling polymorphic behavior.

In Java, an interface is a abstract class that contains only constants, method signatures, and default methods (from Java 8 onwards).
Interfaces are used to define a contract that must be implemented by any class that implements it.
When you create an interface, you are defining a blueprint for other classes to follow.

Now, let’s see how interfaces contribute to polymorphism.
When multiple classes implement the same interface, they can be treated as instances of that interface type.
This allows you to write code that can work with objects of different classes, as long as they implement the same interface.
This is a classic example of polymorphism, where objects of different classes can be treated as objects of a common interface type.

Here’s an example to illustrate this concept

public interface Printable { 
  void print(); 
} 

public class Document implements Printable { 
  public void print() { 
    System.out.println("Printing a document..."); 
  } 
} 

public class Image implements Printable { 
  public void print() { 
    System.out.println("Printing an image..."); 
  } 
} 

public class Main { 
  public static void main(String[] args) { 
    Printable document = new Document(); 
    Printable image = new Image(); 
    print(document); 
    print(image); 
  } 

  public static void print(Printable printable) { 
    printable.print(); 
  } 
}

In this example, Document and Image classes implement the Printable interface, which defines a single method print().
Main class uses a method print(Printable printable) that takes an object of type Printable as an argument.
This method can work with objects of both Document and Image classes, because they implement the Printable interface.
This is an example of polymorphism, where objects of different classes can be treated as objects of a common interface type.

As you can see, interfaces play an important role in achieving polymorphism in Java.
By defining a common interface, you can write code that can work with objects of different classes, making your code more flexible and reusable.

Code Reuse with Polymorphism in Java

All developers aim to write efficient and reusable code, and polymorphism in Java provides an excellent way to achieve this.
By using polymorphism, you can create code that can work with different classes and objects without the need to rewrite or modify the code.

When you use polymorphism, you can define a method or a class that can work with different types of data or objects.
This means that you can write a piece of code once and reuse it multiple times with different inputs or objects.
For example, consider a scenario where you have a method that calculates the area of a shape.
Without polymorphism, you would need to write separate methods for each type of shape, such as a circle, rectangle, or triangle.
However, with polymorphism, you can write a single method that can work with any type of shape, as long as it has a defined area.

Here’s an example of how you can achieve code reuse through polymorphism in Java

public abstract class Shape { 
  public abstract double area(); 
} 

public class Circle extends Shape { 
  private double radius; 

  public Circle(double radius) { 
    this.radius = radius; 
  } 

  @Override 
  public double area() { 
    return Math.PI * radius * radius; 
  } 
} 

public class Rectangle extends Shape { 
  private double length; 
  private double width; 
  public Rectangle(double length, double width) { 
    this.length = length; 
    this.width = width; 
  } 
  
  @Override 
  public double area() { 
    return length * width; 
  } 
} 

public class Main { 
  public static void main(String[] args) { 
    Shape circle = new Circle(5.0); 
    Shape rectangle = new Rectangle(4.0, 6.0); 
    System.out.println("Circle area: " + circle.area()); 
    System.out.println("Rectangle area: " + rectangle.area()); 
  } 
}

In this example, we define an abstract class Shape with an abstract method area().
Then, we create concrete classes Circle and Rectangle that extend the Shape class and implement the area() method.
Finally, in the Main class, you create instances of Circle and Rectangle and call the area() method on them.
By using polymorphism, you can write more generic and reusable code that can work with different classes and objects.
This not only reduces code duplication but also makes your code more flexible and maintainable.

Conclusion

You now have a comprehensive understanding of polymorphism in Java, including its types, implementation, and benefits.
You have learned how to achieve polymorphism through method overloading, method overriding, inheritance, abstract classes, and interfaces.
By applying these concepts, you can write more efficient, flexible, and reusable code.