Polymorphism comes under the four pillars of object-orientated programming. The word "poly" means many, and "morphism" refers to forms. Therefore, polymorphism is the phenomenon that refers to the ability of an object to have multiple forms, that is, to act differently under different conditions.
As in object-orientated programming, we consider objects as real-life entities and form classes accordingly. Similarly, let's understand polymorphism with a real-life example. Consider a person, who can have multiple characteristics at a time, the person can be a father, a son, and an employee at the same time. The same person acts differently in different scenarios, this shows polymorphism.
Now that we have a basic idea about polymorphism, let's dive deeper into the types of polymorphism.
The beauty of polymorphism is that the code working with different classes does
not need to know which class its using since all classes are used the same way. A real-world
example of polymorphism is a button. Everyone knows how to use a button, but what a button operates depends on what it is connected to and the context in which it is used. However, the result does not affect how it is functions.
Let's have a look at the types of polymorphism in the diagram below:
There are generally 2 broad types of Polymorphism:
Static or compile-time polymorphism
Dynamic or run-time polymorphism
Note: Polymorphism can be implemented both within a single class (using compile-time polymorphism) and across multiple classes (using runtime polymorphism).
Let's understand these types of polymorphism.
Static polymorphism, also known as compile-time and ad-hoc polymorphism, is a type of polymorphism that is resolved during the compilation phase of the program. It is achieved through method overloading and operator overloading, where the appropriate method or operator is determined based on the number or types of arguments provided during the compilation.
Static polymorphism is achieved within a class when multiple functions have the same name of the functions but different signatures, that is:
Changing the number of parameters
Changing data types of the arguments
Changing the order of the parameters of the method
public class Person {private String name;private int age;// Constructor with name and agepublic Person(String name) {this.name = name;}public void displayInfo(String occupation) {System.out.println("Occupation: " + occupation);}// Method with different number of parameterspublic void displayInfo(int age, String city) {System.out.println("age: " + age);System.out.println("City: " + city);}// Method with different order of parameterspublic void displayInfo(String city,int age) {System.out.println("City: " + city);System.out.println("age: " + age);}public static void main(String[] args) {Person person1 = new Person("John");Person person2 = new Person("Anya ");// Display additional information (occupation)person1.displayInfo("Software Engineer");System.out.println();// Display additional information (address)person2.displayInfo(25, "New York");person2.displayInfo("New york",25);}}
The Person
class has three versions of the displayInfo
method, each demonstrating method overloading based on the number and types of parameters.
The method with one parameter
The method with two parameters
The method with different parameter order
During compilation, the Java compiler determines which version of the displayInfo
method to call based on the number and types of arguments provided during method invocation. For example, when person1.displayInfo("Software Engineer")
is called, the first version of the displayInfo
method with a String
parameter is invoked. When person2.displayInfo(25, "New York")
and person2.displayInfo("New York",25)
are called, the second and third versions of the displayInfo
method with different parameter orders are invoked, respectively.
Let's now look at how operator overloading works.
#include <iostream>class Complex {private:double real;double imag;public:Complex(double real = 0.0, double imag = 0.0) : real(real), imag(imag) {}// Overload the + operator for complex number additionComplex operator+(const Complex& other) const {return Complex(real + other.real, imag + other.imag);}// Overload the - operator for complex number subtractionComplex operator-(const Complex& other) const {return Complex(real - other.real, imag - other.imag);}// Overload the * operator for complex number multiplicationComplex operator*(const Complex& other) const {return Complex((real * other.real) - (imag * other.imag),(real * other.imag) + (imag * other.real));}// Overload the << operator for custom outputfriend std::ostream& operator<<(std::ostream& os, const Complex& complex) {os << complex.real << " + " << complex.imag << "i";return os; }};int main() {Complex c1(3.0, 4.0);Complex c2(1.5, 2.5);Complex sum = c1 + c2;Complex difference = c1 - c2;Complex product = c1 * c2;std::cout << "c1: " << c1 << std::endl;std::cout << "c2: " << c2 << std::endl;std::cout << "Sum: " << sum << std::endl;std::cout << "Difference: " << difference << std::endl;std::cout << "Product: " << product << std::endl;return 0;}
In the code above, we define a Complex
class to represent complex numbers. The class overloads the +
, -
, and *
operators for complex number addition, subtraction, and multiplication, respectively. We also overload the <<
operator to customize the output of complex numbers when using cout
. In this code, we saw the usage of these overloaded operators by performing arithmetic operations on complex numbers.
Dynamic polymorphism, also known as runtime polymorphism, is a fundamental concept in object-oriented programming (OOP) that allows objects to exhibit different behaviors based on their actual types at runtime. Dynamic polymorphism is achieved through method overriding and virtual functions implementation.
Let's first go through method overriding.
// Base classclass Shape {public void draw() {System.out.println("Drawing a shape");}}// Subclass Circleclass Circle extends Shape {@Overridepublic void draw() {System.out.println("Drawing a circle");}}// Subclass Squareclass Square extends Shape {@Overridepublic void draw() {System.out.println("Drawing a square");}}public class main {public static void main(String[] args) {Shape shape1 = new Circle(); // UpcastingShape shape2 = new Square(); // UpcastingShape shape3 =new Shape();shape1.draw(); // Output: "Drawing a circle"shape2.draw(); // Output: "Drawing a square"shape3.draw();}}
In this example, the Shape
class has a draw
method that is overridden in its Circle
and Square subclasses
. During runtime, the Java virtual machine identifies the actual types of the objects, shape1
and shape2,
and calls the appropriate draw
method based on the object's type. This demonstrates dynamic polymorphism, where the method behavior is determined at runtime based on the actual type of the object, allowing different shapes to be drawn correctly.
Now that we have studied the different types of polymorphism, let's revise their differences in the comparison table below.
Static Polymorphism | Dynamic Polymorphism | |
Binding Time | Compile-Time | Run-time |
Mechanism | Achieved through method overloading. | Achieved through method overriding. |
Class Relationship Class Relationship | Exists within a single class (overloaded methods). | Requires a superclass-subclass relationship (overridden methods). |
Invocation | Early bining | Late binding |
Performance | Slightly faster due to early binding. | Slightly slower due to late binding and dynamic resolution. |
In conclusion, we explored the different types of polymorphism with real-life examples. Polymorphism allows code reusability providing flexibility to developers.