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.