Polymorphism in Java

Polymorphism allows methods to perform differently based on the object that invokes them. It enhances flexibility and maintainability in code. Polymorphism can be classified into two categories:

  1. Compile-time Polymorphism (also known as Method Overloading): Achieved by defining multiple methods in the same class with the same name but different parameter lists.

  2. Runtime Polymorphism (also known as Method Overriding): Occurs when a subclass provides a specific implementation of a method that is already defined in its superclass. The method to be executed is determined at runtime.

Example 1: Method Overloading

Concept: Method overloading is when multiple methods have the same name but different parameter types or counts.

class Calculator {
    // Method overloading: same method name, different parameters
    public int add(int a, int b) {
        return a + b;
    }
 
    public double add(double a, double b) {
        return a + b;
    }
 
    public int add(int a, int b, int c) {
        return a + b + c;
    }
}
 
public class PolymorphismExample1 {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println("Sum (int): " + calc.add(5, 10));         // Calls add(int, int)
        System.out.println("Sum (double): " + calc.add(5.5, 10.5)); // Calls add(double, double)
        System.out.println("Sum (3 ints): " + calc.add(1, 2, 3));   // Calls add(int, int, int)
    }
}

Explanation: The Calculator class defines multiple add methods. Depending on the argument types or count, the appropriate method is invoked. This is determined at compile time.

Example 2: Method Overriding

Concept: Method overriding allows a subclass to provide a specific implementation of a method that is already defined in its superclass.

class Animal {
    void sound() {
        System.out.println("Animals make different sounds");
    }
}
 
class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Dog barks");
    }
}
 
class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("Cat meows");
    }
}
 
public class PolymorphismExample2 {
    public static void main(String[] args) {
        Animal a = new Dog(); // Upcasting
        a.sound();  // Dog's sound() method is called
        
        a = new Cat(); // Reassigning to Cat object
        a.sound();  // Cat's sound() method is called
    }
}

Explanation: The Animal class has a sound method that is overridden by Dog and Cat. The method invoked depends on the actual object type (Dog or Cat), demonstrating runtime polymorphism.

Example 3: Constructor Overloading

Concept: Similar to method overloading, constructor overloading allows multiple constructors with different parameters within the same class.

class Person {
    private String name;
    private int age;
 
    // Overloaded constructors
    public Person() {
        this.name = "Unknown";
        this.age = 0;
    }
 
    public Person(String name) {
        this.name = name;
        this.age = 0;
    }
 
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    public void display() {
        System.out.println("Name: " + name + ", Age: " + age);
    }
}
 
public class PolymorphismExample3 {
    public static void main(String[] args) {
        Person p1 = new Person();
        Person p2 = new Person("John");
        Person p3 = new Person("Jane", 30);
 
        p1.display(); // Output: Name: Unknown, Age: 0
        p2.display(); // Output: Name: John, Age: 0
        p3.display(); // Output: Name: Jane, Age: 30
    }
}

Explanation: The Person class has three constructors, allowing for flexibility when creating objects. Different constructors are called based on the parameters provided.

Example 4: Runtime Polymorphism with Interfaces

Concept: Interfaces allow different classes to implement the same set of methods, enabling polymorphic behavior.

interface Vehicle {
    void drive();
}
 
class Car implements Vehicle {
    public void drive() {
        System.out.println("Car is driving");
    }
}
 
class Bike implements Vehicle {
    public void drive() {
        System.out.println("Bike is driving");
    }
}
 
public class PolymorphismExample4 {
    public static void main(String[] args) {
        Vehicle v = new Car();
        v.drive();  // Car's drive() is called
        
        v = new Bike();
        v.drive();  // Bike's drive() is called
    }
}

Explanation: The Vehicle interface is implemented by Car and Bike. The variable v can reference any object that implements the Vehicle interface, and the method called depends on the actual object type.

Example 5: Overloading with Different Data Types

Concept: Method overloading can also be achieved by changing the parameter types.

class Print {
    public void print(int i) {
        System.out.println("Integer: " + i);
    }
 
    public void print(double d) {
        System.out.println("Double: " + d);
    }
 
    public void print(String s) {
        System.out.println("String: " + s);
    }
}
 
public class PolymorphismExample5 {
    public static void main(String[] args) {
        Print print = new Print();
        print.print(10);          // Calls print(int)
        print.print(10.5);       // Calls print(double)
        print.print("Hello");     // Calls print(String)
    }
}

Explanation: The Print class demonstrates overloading based on different data types. The appropriate print method is called based on the argument type.

Example 6: Polymorphism with Abstract Classes

Concept: Abstract classes can have abstract methods, which must be implemented by subclasses.

abstract class Shape {
    abstract void draw();
}
 
class Circle extends Shape {
    void draw() {
        System.out.println("Drawing a circle");
    }
}
 
class Square extends Shape {
    void draw() {
        System.out.println("Drawing a square");
    }
}
 
public class PolymorphismExample6 {
    public static void main(String[] args) {
        Shape shape = new Circle();
        shape.draw();  // Circle's draw() method is called
        
        shape = new Square();
        shape.draw();  // Square's draw() method is called
    }
}

Explanation: The Shape abstract class defines an abstract method draw. Each subclass (Circle and Square) provides its own implementation. The method called depends on the actual object type.

Example 7: Upcasting in Polymorphism

Concept: Upcasting refers to casting a subclass object to a superclass reference type.

class Animal {
    void sound() {
        System.out.println("Animal makes sound");
    }
}
 
class Dog extends Animal {
    void sound() {
        System.out.println("Dog barks");
    }
}
 
public class PolymorphismExample7 {
    public static void main(String[] args) {
        Animal animal = new Dog();  // Upcasting
        animal.sound();  // Dog's sound() method is called
    }
}

Explanation: Here, the Dog object is referenced by an Animal type variable. This upcasting allows us to invoke the overridden sound method of the Dog class.

Example 8: Polymorphism with Multiple Classes

Concept: Polymorphism allows one interface or abstract class to be implemented by multiple classes.

class Shape {
    void draw() {
        System.out.println("Drawing a shape");
    }
}
 
class Circle extends Shape {
    void draw() {
        System.out.println("Drawing a circle");
    }
}
 
class Triangle extends Shape {
    void draw() {
        System.out.println("Drawing a triangle");
    }
}
 
public class PolymorphismExample8 {
    public static void main(String[] args) {
        Shape shape;
        shape = new Circle();
        shape.draw();  // Circle's draw() method is called
 
        shape = new Triangle();
        shape.draw();  // Triangle's draw() method is called
    }
}

Explanation: The Shape class is the base class, and both Circle and Triangle provide their own implementations of draw(). The method invoked depends on the actual object assigned to shape.

Example 9: Polymorphism with Collections

Concept: Collections can hold references to objects of different types, demonstrating polymorphic behavior.

import java.util.ArrayList;
import java.util.List;
 
class Animal {
    void sound() {
        System.out.println("Animal makes sound");
    }
}
 
class Dog extends Animal {
    void sound() {
        System.out.println("Dog barks");
    }
}
 
class Cat extends Animal {
    void sound() {
        System.out.println("Cat meows");
    }
}
 
public class PolymorphismExample9 {
    public static void main(String[] args) {
        List<Animal> animals = new ArrayList
 
<>();
        animals.add(new Dog());
        animals.add(new Cat());
 
        for (Animal animal : animals) {
            animal.sound();  // Polymorphic behavior
        }
    }
}

Explanation: A list of Animal references can contain Dog and Cat objects. The appropriate sound method is called based on the actual object type, demonstrating polymorphism in collections.

Example 10: Overloading with Varargs

Concept: Varargs allows a method to accept variable-length arguments, enabling flexibility in method calls.

class Adder {
    public int add(int... numbers) {
        int sum = 0;
        for (int num : numbers) {
            sum += num;
        }
        return sum;
    }
}
 
public class PolymorphismExample10 {
    public static void main(String[] args) {
        Adder adder = new Adder();
        System.out.println("Sum: " + adder.add(1, 2, 3, 4, 5)); // Calls add(int...)
    }
}

Explanation: The add method accepts a variable number of integer arguments. This method can be called with any number of arguments, enhancing flexibility.

Example 11: Runtime Polymorphism with Superclass Reference

Concept: The superclass reference can hold the subclass object, allowing for dynamic method dispatch.

class Employee {
    void work() {
        System.out.println("Employee is working");
    }
}
 
class Manager extends Employee {
    void work() {
        System.out.println("Manager is managing");
    }
}
 
public class PolymorphismExample11 {
    public static void main(String[] args) {
        Employee emp = new Manager();  // Upcasting
        emp.work();  // Manager's work() method is called
    }
}

Explanation: The Employee reference emp holds a Manager object. When work() is called, the Manager's overridden method is executed due to dynamic binding.

Example 12: Polymorphism with Final Methods

Concept: Methods declared as final cannot be overridden, maintaining their behavior across subclasses.

class Vehicle {
    final void run() {
        System.out.println("Vehicle is running");
    }
}
 
class Car extends Vehicle {
    // run() cannot be overridden because it's final in Vehicle
}
 
public class PolymorphismExample12 {
    public static void main(String[] args) {
        Vehicle vehicle = new Car();
        vehicle.run();  // Calls Vehicle's final run() method
    }
}

Explanation: The run method in Vehicle is final, so it cannot be overridden by Car. When run is called, the original implementation from Vehicle is executed.

Example 13: Covariant Return Type in Polymorphism

Concept: Covariant return types allow a method to return a subclass type instead of the superclass type.

class Animal {
    Animal get() {
        return this;
    }
}
 
class Dog extends Animal {
    @Override
    Dog get() {
        return this; // Returns Dog instance
    }
}
 
public class PolymorphismExample13 {
    public static void main(String[] args) {
        Animal animal = new Dog();
        Dog dog = (Dog) animal.get(); // Downcasting
        System.out.println("Dog instance: " + dog);
    }
}

Explanation: The get method in Dog returns a Dog type instead of Animal, demonstrating covariant return types. The animal reference can call get, returning a Dog instance.

Example 14: Dynamic Method Dispatch

Concept: Java uses dynamic method dispatch to resolve method calls at runtime based on the object's actual type.

class A {
    void show() {
        System.out.println("Class A");
    }
}
 
class B extends A {
    void show() {
        System.out.println("Class B");
    }
}
 
public class PolymorphismExample14 {
    public static void main(String[] args) {
        A obj = new B();  // Upcasting
        obj.show();       // Class B is printed due to dynamic method dispatch
    }
}

Explanation: In this example, obj is an A type reference but holds a B type object. The show method executed is from class B, showcasing dynamic method dispatch.

Example 15: Polymorphism with Getters and Setters

Concept: Getters and setters can be polymorphic by changing the return type.

class Employee {
    String getDetails() {
        return "Employee details";
    }
}
 
class Manager extends Employee {
    @Override
    String getDetails() {
        return "Manager details";
    }
}
 
public class PolymorphismExample15 {
    public static void main(String[] args) {
        Employee emp = new Manager(); // Upcasting
        System.out.println(emp.getDetails());  // Calls Manager's getDetails()
    }
}

Explanation: The getDetails method in Employee is overridden in Manager. When getDetails is called on emp, the Manager version is executed, illustrating polymorphic behavior.

Example 16: Polymorphism with Static Methods

Concept: Static methods cannot be overridden; instead, they are hidden.

class Parent {
    static void display() {
        System.out.println("Parent's display method");
    }
}
 
class Child extends Parent {
    static void display() {
        System.out.println("Child's display method");
    }
}
 
public class PolymorphismExample16 {
    public static void main(String[] args) {
        Parent obj = new Child();
        obj.display(); // Calls Parent's display() method due to static method hiding
    }
}

Explanation: In this example, display is a static method. When called on a Parent reference pointing to a Child object, it calls the Parent's version, showcasing static method hiding.

Example 17: Polymorphism in Event Handling

Concept: Polymorphism is commonly used in event handling, where a single event can have multiple handlers.

import java.awt.*;
import java.awt.event.*;
 
public class PolymorphismExample17 {
    public static void main(String[] args) {
        Frame f = new Frame("Polymorphism Example");
        Button b = new Button("Click Me");
 
        // Using polymorphism in event handling
        b.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                System.out.println("Button clicked!");
            }
        });
 
        f.add(b);
        f.setSize(300, 300);
        f.setLayout(new FlowLayout());
        f.setVisible(true);
    }
}

Explanation: The ActionListener interface is implemented by an anonymous class. The button's click event can be handled polymorphically, allowing for different actions to occur based on different event sources.

Example 18: Polymorphism with Lambda Expressions

Concept: Lambda expressions enable polymorphic behavior in functional interfaces.

import java.util.function.Function;
 
public class PolymorphismExample18 {
    public static void main(String[] args) {
        Function<Integer, Integer> square = x -> x * x;
        Function<Integer, Integer> cube = x -> x * x * x;
 
        System.out.println("Square: " + square.apply(5)); // Output: Square: 25
        System.out.println("Cube: " + cube.apply(3));     // Output: Cube: 27
    }
}

Explanation: Here, two lambda expressions are assigned to a Function interface. This allows different behavior (squaring and cubing) to be executed based on which lambda expression is called.

Example 19: Polymorphism in GUI Applications

Concept: In GUI applications, polymorphism allows components to behave differently based on their types.

import javax.swing.*;
import java.awt.event.*;
 
public class PolymorphismExample19 {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Polymorphism Example");
        JButton button = new JButton("Press Me");
        JTextField textField = new JTextField(20);
 
        // Using polymorphism for event handling
        button.addActionListener(e -> textField.setText("Button Pressed!"));
 
        frame.add(button);
        frame.add(textField);
        frame.setSize(300, 300);
        frame.setLayout(new FlowLayout());
        frame.setVisible(true);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
}

Explanation: This example demonstrates using polymorphism in GUI components. The button click modifies the text field, showing how different components can respond to events polymorphically.

Example 20: Polymorphism in Data Processing

Concept: Data processing classes can use polymorphism to handle various data formats.

abstract class DataProcessor {
    abstract void process();
}
 
class CSVProcessor extends DataProcessor {
    void process() {
        System.out.println("Processing CSV data");
    }
}
 
class JSONProcessor extends DataProcessor {
    void process() {
        System.out.println("Processing JSON data");
    }
}
 
public class PolymorphismExample20 {
    public static void main(String[] args) {
        DataProcessor processor;
 
        processor = new CSVProcessor();
        processor.process(); // Processing CSV data
 
        processor = new JSONProcessor();
        processor.process(); // Processing JSON data
    }
}

Explanation: The DataProcessor abstract class defines the process method. CSVProcessor and JSONProcessor provide specific implementations, allowing

different data formats to be processed polymorphically.