Java Polymorphism

1. Introduction

In this lesson of Java course, we will look at the Java Polymorphism. Polymorphism, derived from the Greek words “poly” (many) and “morph” (form), is a core principle of OOP that allows objects of different classes to be treated as if they were objects of the same class. It facilitates the creation of more flexible and scalable code by promoting code reusability and modularity.

2. Java Polymorphism- Method Overriding

java polymorphism

Method overriding is a good example of Java polymorphism. It’s a mechanism that allows a subclass to provide a specific implementation for a method that is already defined in its super-class. The method in the subclass should have the same name, return type, and parameters as the method in the super-class. This enables runtime polymorphism, where the appropriate method is dynamically selected based on the object’s actual type at runtime.

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

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

class Square extends Shape {
    @Override
    void draw() {
        System.out.println("Drawing a square");
    }
}

public class Main {
    public static void main(String[] args) {
        Shape shape1 = new Circle();
        Shape shape2 = new Square();
        
        shape1.draw(); // Output: Drawing a circle
        shape2.draw(); // Output: Drawing a square
    }
}

In this example, both Circle and Square are subclasses of shape. The draw method is overridden in each subclass to provide specialized implementations. At runtime, the draw method is called based on the actual type of the object.

2.1. Use Cases and Benefits

Let’s look at some use cases and benefits of Java polymorphism.Method overriding allows for the creation of more specific and specialized behavior in subclasses while maintaining a common interface in the superclass. This is useful for creating extensible and adaptable code. For instance, a base class like Shape can have a general draw method, and subclasses like Circle and Square can provide their own implementations tailored to their unique characteristics.

3. Interface Polymorphism

Besides method overriding, Java polymorphism is also supported through interface implementation. An interface defines a contract that specifies a set of methods that implementing classes must provide.

3.1. Defining Interfaces

interface Sound {
    void makeSound();
}

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

class Cat implements Sound {
    @Override
    public void makeSound() {
        System.out.println("Cat meows");
    }
}

public class Main {
    public static void main(String[] args) {
        Sound dog = new Dog();
        Sound cat = new Cat();
        
        dog.makeSound(); // Output: Dog barks
        cat.makeSound(); // Output: Cat meows
    }
}

In this example, the Sound interface defines a makeSound method that must be implemented by any class that implements the interface. The Dog and Cat classes provide their own implementations of the makeSound method.

3.2. Implementing Multiple Interfaces

A class in Java can implement multiple interfaces, allowing it to exhibit polymorphic behavior based on the interfaces it implements.

interface Swimmer {
    void swim();
}

interface Flyer {
    void fly();
}

class Duck implements Swimmer, Flyer {
    @Override
    public void swim() {
        System.out.println("Duck swims");
    }
    
    @Override
    public void fly() {
        System.out.println("Duck flies");
    }
}

public class Main {
    public static void main(String[] args) {
        Duck duck = new Duck();
        
        duck.swim(); // Output: Duck swims
        duck.fly();  // Output: Duck flies
    }
}

4. Runtime Polymorphism

Runtime polymorphism, also known as dynamic Java polymorphism, occurs when a method call is resolved at runtime based on the actual type of the object. This is achieved through a mechanism called dynamic method dispatch.

4.1. Dynamic Method Dispatch

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

class Car extends Vehicle {
  @Override
  void makeSound() {
    System.out.println("Car honks");
  }
}

class Bike extends Vehicle {
  @Override
  void makeSound() {
    System.out.println("Bike roars");
  }
}

public class Main {
  public static void main(String[] args) {
    Vehicle vehicle1 = new Car();
    Vehicle vehicle2 = new Bike();

    vehicle1.makeSound(); // Output: Car honks
    vehicle2.makeSound(); // Output: Bike roars
  }
}

In this example, the makeSound method is overridden in both the Dog and Cat subclasses. When an object of the superclass Animal is assigned a reference to a Dog or Cat object, the appropriate makeSound method is dynamically dispatched based on the actual type of the object.

4.2. The ‘super‘ Keyword

The super keyword is used to call a method explicitly from the superclass when the method is overridden in the subclass.

class Parent {
    void display() {
        System.out.println("Parent's display method");
    }
}

class Child extends Parent {
    @Override
    void display() {
        super.display(); // Calling the parent class's method
        System.out.println("Child's display method");
    }
}

public class Main {
    public static void main(String[] args) {
        Child child = new Child();
        child.display();
    }
}

5. Polymorphism vs. Overloading

Polymorphism is often confused with method overloading, but they are distinct concepts. Method overloading refers to defining more than one method in the same class with the same name but different parameter lists. Overloaded methods are resolved at compile-time based on the number and types of arguments passed to them.

Polymorphism involves overriding methods in subclasses to provide specific implementations. The method to be executed is determined at runtime based on the actual type of the object.

6. Polymorphism in Real-World Scenarios

Java Polymorphism finds extensive use in real-world scenarios, such as

  • GUI frameworks, where various UI elements share a common interface for handling events.
  • Database abstraction layers where different database engines implement a common set of methods.
  • Plugin architectures that allow the addition of new features without altering the existing codebase.

7. Why do we need Java Polymorphism?

  • Code Reusability: Polymorphism allows you to reuse code by defining common interfaces or base classes that can be shared among multiple derived classes, reducing duplication.
  • Flexibility: It enables the creation of code that can work with a wide range of different objects, promoting adaptability and extensibility.
  • Abstraction: Polymorphism allows you to work with objects at a higher level of abstraction, focusing on what objects can do rather than their specific implementations.
  • Simplifies Code: It simplifies complex systems by providing a consistent interface for interacting with diverse objects, making the code easier to understand and maintain.
  • Scalability: New classes and implementations can be added without altering the existing code, facilitating the expansion of software functionality.
  • Promotes Modularity: Polymorphism encourages modular design, where each class has a well-defined purpose and can be developed and tested independently.
  • Enhances Readability: Code that uses polymorphism is often more readable and self-explanatory, as it focuses on the desired behavior rather than low-level details.
  • Enables Polymorphic Behavior: Polymorphism allows for the creation of functions or methods that can work with objects of different classes, improving code efficiency and reducing redundancy.
  • Supports Inheritance: Polymorphism is closely tied to inheritance, enabling the overriding of methods in derived classes to provide specialized behavior while adhering to a common interface.

8. Conclusion

Polymorphism is a powerful concept in Java that enables code reuse, extensibility, and flexibility. In this lesson, we learned about the Java Polymorphism through method overriding and interface implementation. Developers can create adaptable and modular code that can be easily extended and changed. By understanding and effectively using polymorphism, Java programmers can build more efficient and maintainable applications. As always, the source code for this article is available on GitHub.