Java Polymorphism – Types And Examples

Polymorphism is one of the 4 pillars of Object-Oriented Programming. It is a combination of two Greek words: poly and morphs. “Poly” means “many,” and “morphs” means “forms.” So in Java, polymorphism means many forms. Polymorphism is defined as the ability of a message to be displayed in more than one form.

Let’s understand the meaning of polymorphism by an example:

Consider a woman in society. The same woman performs different roles in society. The woman can be the wife of someone, the mother of her child, can be at the role of a manager in an organization, and many more at the same time. But the Woman is only one. So, the same woman performing different roles is polymorphism.

Another best example is your smartphone. The smartphone can act as a phone, camera, music player, alarm, and whatnot, taking different forms and hence polymorphism

Introduction

Polymorphism in Java refers to an object’s ability to take on multiple forms or types. It enables the treatment of objects of different classes as objects of a common superclass or interface. In simpler terms, polymorphism allows us to represent different types of objects using a single variable or method. 

We can relate polymorphism in real life by the following example. Consider different types of animals. Each animal makes a distinct sound. By leveraging polymorphism, you can define a common “makeSound” method in a superclass called “Animal,” which is overridden in each subclass with the specific sound implementation.

// Base class Animal
class Animal {
    public void makeSound() {
        System.out.println("Animal making a generic sound...");
    }
}
// Subclass Dog
class Dog extends Animal {
    // Override the makeSound method in the Dog class
    public void makeSound() {
        System.out.println("Dog barking: Woof!");
    }
}
// Subclass Cat
class Cat extends Animal {
    // Override the makeSound method in the Cat class
    public void makeSound() {
        System.out.println("Cat meowing: Meow!");
    }
}

// Subclass Cow
class Cow extends Animal {
    // Override the makeSound method in the Cow class
    public void makeSound() {
        System.out.println("Cow mooing: Moo!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();
        Animal cow = new Cow();

        dog.makeSound();
        cat.makeSound();
        cow.makeSound();
    }
}

Output:

Dog barking: Woof!
Cat meowing: Meow!
Cow mooing: Moo!

In this example, we have a base class Animal with a makeSound method. The subclasses Dog, Cat, and Cow inherit from the Animal class and override the makeSound method with their specific sound. By creating objects of each subclass and calling the makeSound method, we observe polymorphic behavior where each animal produces its unique sound.

This example demonstrates how polymorphism allows for the flexibility of handling different objects through a common interface or superclass, enabling code reuse and promoting flexibility in real-life scenarios.

How polymorphism can be achieved?

Polymorphism in Java can be achieved through two concepts i.e. method overloading and method overriding. Let’s dive into each of these concepts:

Method Overloading

Polymorphism via method overloading enables a class to have multiple methods with the same name but different parameters. The methods can perform similar actions but with different data types or parameters. The Java compiler determines which method to invoke based on the method call arguments.

Example:

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

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

public class Main {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        System.out.println(calculator.add(5, 10)); // Output: 15
        System.out.println(calculator.add(3.5, 2.5)); // Output: 6.0
    }
}

In the example above, the Calculator class has two add() methods. One accepts two integers as parameters, while the other accepts two doubles. Depending on the arguments passed during the method call, the compiler determines the appropriate method to invoke. This allows us to use the same method name add() for different data types, enhancing code readability and flexibility.

Method Overriding

Polymorphism through method overriding allows a subclass to provide its own implementation of a method defined in its parent class. It involves creating a method in the subclass with the same name, return type, and parameters as the method in the parent class. The method in the subclass overrides the implementation of the method in the parent class, allowing the subclass to exhibit its unique behavior.

Example:

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

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

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

In the example above, we have a superclass called Animal with a makeSound() method. The Dog class extends the Animal class and overrides the makeSound() method with its own implementation. When we create an object of type Animal and assign it to a Dog object, and then call the makeSound() method, it invokes the overridden method in the Dog class, displaying “Dog barks” as the output.

Types Of Polymorphism In Java

There are two types of polymorphism in Java: compile-time polymorphism (also known as method overloading) and runtime polymorphism (also known as method overriding).

  1. Compile-Time Polymorphism (Method Overloading)

This type of polymorphism in Java is also called static polymorphism or static method dispatch. It can be achieved by method overloading. In this process, an overloaded method is resolved at compile time rather than resolving at runtime. 

  • Method overloading allows a class to have multiple methods with the same name but different parameters.
  • Based on the number and type of the arguments passed the compiler determines which method to call.
  • The methods may have different return types, but this alone is insufficient to distinguish overloaded methods.

Example 1: Method Overloading with Different Number of Parameters

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

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

    public static void main(String[] args) {
        Calculator calculator = new Calculator();

        System.out.println(calculator.add(2, 3));           
        System.out.println(calculator.add(2, 3, 4));       
    }
}

Output:

5
9

In this example, the Calculator class has two add methods. The first add method takes two parameters, a and b, and returns their sum. The second add method takes three parameters, a, b, and c, and returns their sum. The two methods have the same name but different numbers of parameters. Depending on the number of arguments passed the compiler selects the appropriate method.

Example 2: Method Overloading with Different Types of Parameters

public class Converter {
    public String convertToString(int number) {
        return String.valueOf(number);
    }

    public String convertToString(double number) {
        return String.valueOf(number);
    }

    public static void main(String[] args) {
        Converter converter = new Converter();

        System.out.println(converter.convertToString(42));         
        System.out.println(converter.convertToString(3.14));      
    }
}

Output:

42
3.14

In this example, the Converter class has two convertToString methods. The first convertToString method takes an int parameter and converts it to a String. The second convertToString method takes a double parameter and converts it to a String. The two methods have the same name but different types of parameters. Based on the type of the argument passed the compiler determines which method to invoke.

  1. Runtime Polymorphism (Method Overriding)

Dynamic method dispatch is another name for runtime polymorphism. This polymorphism is achieved by method overriding. The overridden method is resolved at runtime rather than at compile time.  

In Java, runtime polymorphism occurs when two or more classes are related through inheritance. We must create an “IS-A” relationship between classes and override a method to achieve runtime polymorphism.

  • Method overriding occurs when a subclass implements a method that is already defined in the parent class.
  • The must rule of method overriding is that the subclass method must have the same name, return type, and parameter list as the parent class method.
  • The method to invoke is determined at runtime based on the actual type of the object.
  • The @Override annotation (optional but recommended) is frequently used to indicate that a method is intended to override a superclass method.

Example 1: Method Overriding

class Animal {
    public void makeSound() {
        System.out.println("Animal is making a sound");
    }
}

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Cat is meowing");
    }
}

class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog is barking");
    }
}

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

        animal1.makeSound(); 
        animal2.makeSound(); 
    }
}

Output:

Cat is meowing
Dog is barking

In this example, we have an Animal class with a makeSound method. The Cat and Dog classes extend the Animal class and override the makeSound method with their own specific sound. At runtime, when we create an instance of Cat and assign it to an Animal reference, and call the makeSound method, the overridden makeSound method of Cat is invoked. Similarly, when we create an instance of Dog and call the makeSound method, the overridden makeSound method of Dog is invoked. This demonstrates runtime polymorphism, where the behavior of the makeSound method is determined by the actual type of the object.

Example 2: Overriding Data Members

class Vehicle {
    protected String type = "Vehicle";
}

class Car extends Vehicle {
    protected String type = "Car";
}

public class Main {
    public static void main(String[] args) {
        Vehicle vehicle = new Car();
        System.out.println(vehicle.type); 
    }
}

Output:

Vehicle

In this example, we have a Vehicle class with a type data member. The Car class extends the Vehicle class and also declares its own type data member. When we create an instance of Car and assign it to a Vehicle reference, and access the type data member, the value of the type data member in the Vehicle class is accessed. This is because data members cannot be overridden in Java, they can only be hidden or shadowed. Therefore, the output will be “Vehicle” instead of “Car”.

Differences between Compile-Time and Runtime Polymorphism

Compile-time Polymorphism (Method Overloading)Runtime Polymorphism (Method Overriding
Occurs at compile-timeOccurs at runtime
Decision based on method signature and argumentsDecision based on actual object type
Multiple methods with the same name, different parametersSubclass provides a different implementation of a method in superclass
Method resolution by compilerMethod resolution by JVM at runtime
Number, types, and order of arguments determine method selectionObject type determines method selection
Static binding (early binding)Dynamic binding (late binding)
Achieved through method overloadingAchieved through method overriding
Provides flexibility by performing different tasks based on argumentsPromotes extensibility and specialization in OOP

Characteristics Of Polymorphism

Besides method overloading and method overriding, polymorphism has other characteristics as follows.

  1. Coercion
  2. Internal Operator Overloading
  3. Polymorphic Variables or Parameters
  4. Subtype polymorphism
  1. Coercion

Automatic conversion of one data type to another is referred to as Coercion. It allows for seamless operations between different data types. Let’s consider an example:

int num1 = 5;
double num2 = 2.5;

double result = num1 + num2;
System.out.println(result);

In this example, the int type variable num1 is automatically coerced to a double type to perform the addition operation. The result is stored in the double variable result, which is then printed. Here, the coercion allows for the addition of two different data types.

  1. Internal Operator Overloading

The ability of operators to behave differently depending on the data types involved is referred to as internal operator overloading. Let’s see an example:

String str1 = "Hello";
String str2 = " World";

String result = str1 + str2;
System.out.println(result);

In this example, the + operator is used for concatenating two strings instead of performing addition. This is an example of internal operator overloading, where the behavior of the operator is determined by the data types involved.

  1. Polymorphic Variables

Polymorphic variables allow a single variable to hold objects of different types, as long as they are related through inheritance. Here’s an example:

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

class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

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

        shape1.draw();
        shape2.draw();
    }
}

In this example, the Shape class is a superclass, and Circle and Rectangle are its subclasses. The shape1 and shape2 variables are of type Shape but hold objects of different subclasses. When the draw() method is called on each variable, the appropriate implementation of the method in the respective subclass is executed. This is an example of polymorphic variables, where a single variable can represent different types of objects.

  1. Subtype Polymorphism:

Subtype polymorphism allows objects of derived classes to be treated as objects of their superclass. This characteristic enables code to be written in a more general way, operating on objects of different subclasses through a superclass reference. Let’s consider an example:

abstract class Animal {
    public abstract void makeSound();
}

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

class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Meow!");
    }
}

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

        animal1.makeSound();
        animal2.makeSound();
    }
}

In this example, the Animal class is an abstract superclass, and Dog and Cat are its concrete subclasses. The animal1 and animal2 variables, of type Animal, can refer to objects of different subclasses. When the makeSound() method is called on each variable, the appropriate implementation in the corresponding subclass is executed. This demonstrates the subtype polymorphism, where objects of different subclasses are treated as objects of their common superclass.

Advantages Of Polymorphism

  1. Code Reusability: Polymorphism allows you to reuse code by creating a common interface or superclass that multiple classes can implement or extend. This reduces the need for writing duplicate code and promotes efficient development.
  1. Flexibility and Extensibility: Polymorphism enables you to create flexible and extensible code. You can add new classes that inherit from a common superclass or implement a common interface without affecting the existing code. This makes it easier to accommodate future changes or requirements.
  1. Enhanced Maintainability: Polymorphic code is easier to maintain. If you need to make changes or fix issues, you can do so in the superclass or interface, and the changes will automatically reflect in all the subclasses or implementations. This reduces the risk of introducing errors and simplifies code maintenance.
  1. Improved Readability: Polymorphic code promotes clean and readable code. By utilizing a common interface or superclass, you can write code that focuses on the common behavior shared by multiple classes. This improves code readability and makes it easier to understand the overall logic.

Disadvantages Of Polymorphism

  1. Runtime Performance Overhead: Polymorphism involves dynamic dispatch, where the appropriate method implementation is determined at runtime. This additional lookup process can incur a slight performance overhead compared to direct method invocations. However, the impact is usually negligible in most applications.
  1. Complexity and Learning Curve: Understanding and effectively implementing polymorphism requires a solid understanding of object-oriented principles, such as inheritance and interfaces. It may initially introduce complexity for beginners who are new to these concepts. However, with practice and experience, it becomes easier to grasp and utilize effectively.
  1. Design Constraints: Polymorphism requires careful design and planning to ensure that the hierarchy or interface relationships are well-defined. Making changes to the superclass or interface can have implications on the entire codebase, requiring thoughtful consideration and testing to avoid unintended consequences.
  1. Potential for Misuse: Polymorphism can be misused if not applied appropriately. Overuse or improper design decisions can lead to overly complex code and reduced maintainability. It’s important to understand when and where to apply polymorphism to avoid unnecessary complexity and ensure a clean and efficient code structure.

Conclusion

  • Polymorphism is a powerful concept in Java that allows objects of different classes to be treated as objects of a common superclass or interface.
  • Polymorphism is achieved through inheritance and interfaces. Inheritance allows a class to inherit properties and behaviors from its parent class, while interfaces define a contract for classes to implement specific methods.
  • There are two main types of polymorphism in Java: compile-time polymorphism (method overloading) and runtime polymorphism (method overriding).
  • Compile-time polymorphism is achieved when there are multiple methods with the same name but different parameters in a class. The appropriate method is determined based on the arguments used during compilation.
  • Runtime polymorphism is achieved when a subclass overrides a method of its superclass. The appropriate method is determined dynamically at runtime based on the actual object type.
  • The characteristics of polymorphism include coercion, internal operator overloading, polymorphic variables, and subtype polymorphism.
  • Coercion refers to the automatic conversion of one data type to another, allowing flexibility in assigning values.
  • Internal operator overloading allows operators to exhibit different behaviors based on the operands involved.
  • Polymorphic variables can hold objects of different types, providing flexibility and adaptability in code.
  • Subtype polymorphism enables a subclass to be treated as an instance of its superclass, allowing for code reuse and flexibility.

In conclusion, polymorphisms is a key feature of Java that enables code reuse, flexibility, and extensibility. By leveraging inheritance, interfaces, and method overriding, you can write more adaptable and maintainable code. Understanding the types and characteristics of polymorphism is essential for utilizing this feature effectively in your Java programs.

Leave a Reply

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