Understanding Polymorphism in Java

Introduction

Polymorphism is one of the core concepts of Object-Oriented Programming (OOP) that plays a pivotal role in making Java a powerful and flexible language. It allows objects to be treated as instances of their parent class rather than their actual class. The word "polymorphism" means "many shapes" and it enables a single interface to be used for a general class of actions.

What is Polymorphism?

Polymorphism in Java allows one interface to be used for different data types. It can be divided into two types:

  1. Compile-time Polymorphism (Method Overloading)

  2. Run-time Polymorphism (Method Overriding)

Compile-time Polymorphism (Method Overloading)

Method Overloading occurs when multiple methods in the same class have the same name but different parameters. The method to be called is determined at compile-time based on the method signature.

Run-time Polymorphism (Method Overriding)

Method Overriding occurs when a subclass provides a specific implementation of a method that is already defined in its superclass. The method call is determined at runtime based on the object type.

Fundamentals of Polymorphism

Polymorphism is based on the inheritance hierarchy. When a method in a subclass overrides a method in its superclass, the subclass version of the method is executed.

Benefits of Polymorphism

  1. Code Reusability: Polymorphism promotes the reusability of code.

  2. Maintainability: It makes the code easier to maintain.

  3. Flexibility: It allows for flexible and extendable code.

  4. Extensibility: New functionality can be easily added by creating new subclasses.

Inheritance and Variable Scope

Inheritance is the mechanism by which one class inherits the fields and methods of another class. In Java, a class can inherit from only one superclass but can implement multiple interfaces.

  • Super Keyword: Used to refer to the immediate parent class object.

  • This Keyword: Refers to the current class instance.

  • Scope of Variables: Class variables (static) and instance variables have different scopes. Local variables inside methods are not accessible outside their scope.

Method Overloading vs Method Overriding

Method Overloading

  • Same method name but different parameters.

  • Resolved at compile-time.

  • It can occur within the same class.

Method Overriding

  • Same method name, the same parameters.

  • Resolved at runtime.

  • Occurs between superclass and subclass.

Practical Examples

Simple Examples

Example 1: Compile-time Polymorphism (Overloading)

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

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

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

public class TestOverloading {
    public static void main(String[] args) {
        MathOperations math = new MathOperations();
        System.out.println(math.add(5, 10)); // Output: 15
        System.out.println(math.add(5.5, 2.2)); // Output: 7.7
        System.out.println(math.add(1, 2, 3)); // Output: 6
    }
}

Example 2: Run-time Polymorphism (Overriding)

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

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

public class TestOverriding {
    public static void main(String[] args) {
        Animal myDog = new Dog();
        myDog.sound(); // Output: Dog barks
    }
}

Example 3: Interface Polymorphism

interface Shape {
    void draw();
}

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

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

public class TestInterfacePolymorphism {
    public static void main(String[] args) {
        Shape myShape = new Circle();
        myShape.draw(); // Output: Drawing Circle

        myShape = new Rectangle();
        myShape.draw(); // Output: Drawing Rectangle
    }
}

Complex Examples

Example 1: Polymorphism in Collections

import java.util.ArrayList;
import java.util.List;

abstract class Employee {
    abstract void work();
}

class Developer extends Employee {
    @Override
    void work() {
        System.out.println("Developer is coding");
    }
}

class Designer extends Employee {
    @Override
    void work() {
        System.out.println("Designer is designing");
    }
}

public class TestPolymorphismInCollections {
    public static void main(String[] args) {
        List<Employee> employees = new ArrayList<>();
        employees.add(new Developer());
        employees.add(new Designer());

        for (Employee emp : employees) {
            emp.work();
        }
    }
}

Example 2: Polymorphism in a Payment System

abstract class Payment {
    abstract void pay();
}

class CreditCardPayment extends Payment {
    @Override
    void pay() {
        System.out.println("Payment made by credit card");
    }
}

class PayPalPayment extends Payment {
    @Override
    void pay() {
        System.out.println("Payment made by PayPal");
    }
}

public class TestPaymentSystem {
    public static void main(String[] args) {
        Payment payment = new CreditCardPayment();
        payment.pay(); // Output: Payment made by credit card

        payment = new PayPalPayment();
        payment.pay(); // Output: Payment made by PayPal
    }
}

Example 3: Polymorphism in a Notification System

abstract class Notification {
    abstract void notifyUser();
}

class EmailNotification extends Notification {
    @Override
    void notifyUser() {
        System.out.println("Notifying user via Email");
    }
}

class SMSNotification extends Notification {
    @Override
    void notifyUser() {
        System.out.println("Notifying user via SMS");
    }
}

public class TestNotificationSystem {
    public static void main(String[] args) {
        Notification notification = new EmailNotification();
        notification.notifyUser(); // Output: Notifying user via Email

        notification = new SMSNotification();
        notification.notifyUser(); // Output: Notifying user via SMS
    }
}

Examples without Polymorphism vs. with Polymorphism

Without Polymorphism

class Car {
    void start() {
        System.out.println("Car starts");
    }
}

class Bike {
    void start() {
        System.out.println("Bike starts");
    }
}

public class TestWithoutPolymorphism {
    public static void main(String[] args) {
        Car car = new Car();
        car.start(); // Output: Car starts

        Bike bike = new Bike();
        bike.start(); // Output: Bike starts
    }
}

With Polymorphism

abstract class Vehicle {
    abstract void start();
}

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

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

public class TestWithPolymorphism {
    public static void main(String[] args) {
        Vehicle vehicle = new Car();
        vehicle.start(); // Output: Car starts

        vehicle = new Bike();
        vehicle.start(); // Output: Bike starts
    }
}

Real-world Use Case: Polymorphism in a Graphic Editor

Imagine a graphic editor that can handle various shapes like circles, rectangles, and triangles. Using polymorphism, you can define a common interface for all shapes and create specific implementations for each shape.

Shape Interface and Implementations

interface Shape {
    void draw();
}

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

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

class Triangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing Triangle");
    }
}

Graphic Editor

import java.util.ArrayList;
import java.util.List;

public class GraphicEditor {
    private List<Shape> shapes = new ArrayList<>();

    public void addShape(Shape shape) {
        shapes.add(shape);
    }

    public void drawShapes() {
        for (Shape shape : shapes) {
            shape.draw();
        }
    }

    public static void main(String[] args) {
        GraphicEditor editor = new GraphicEditor();
        editor.addShape(new Circle());
        editor.addShape(new Rectangle());
        editor.addShape(new Triangle());

        editor.drawShapes(); // Output: Drawing Circle, Drawing Rectangle, Drawing Triangle
    }
}

Conclusion

Polymorphism is a fundamental concept in Java and OOP that allows objects to be treated as instances of their parent class rather than their actual class. This promotes code reusability, flexibility, and maintainability. Developers can create more robust and scalable applications by understanding and utilizing polymorphism.