1. java
  2. /oop
  3. /polymorphism

Master Java Polymorphism and Dynamic Method Dispatch

Java Polymorphism

Polymorphism is one of the core principles of object-oriented programming that allows objects of different types to be treated as instances of the same type through a common interface. The word "polymorphism" comes from Greek, meaning "many forms."

What is Polymorphism?

Polymorphism enables a single interface to represent different underlying forms (data types). It allows you to write more flexible and reusable code by working with objects at a more abstract level.

// Common interface
public abstract class Animal {
    public abstract void makeSound();
}

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

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

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

// Polymorphic usage
public class AnimalShelter {
    public void makeAllAnimalsSound(Animal[] animals) {
        for (Animal animal : animals) {
            animal.makeSound(); // Polymorphic method call
        }
    }
}

// Usage
Animal[] animals = {
    new Dog(),
    new Cat(), 
    new Bird()
};

AnimalShelter shelter = new AnimalShelter();
shelter.makeAllAnimalsSound(animals);
// Output: Woof! Meow! Tweet!

Types of Polymorphism

1. Compile-Time Polymorphism (Static Polymorphism)

Resolved during compilation through method overloading.

Method Overloading

public class Calculator {
    // 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 String add(String a, String b) {
        return a + b;
    }
}

// Usage
Calculator calc = new Calculator();
System.out.println(calc.add(5, 3));        // Calls int version
System.out.println(calc.add(5.5, 3.2));    // Calls double version
System.out.println(calc.add(1, 2, 3));     // Calls three-parameter version
System.out.println(calc.add("Hello", " World")); // Calls String version

Constructor Overloading

public class Person {
    private String name;
    private int age;
    private String email;
    
    // Default constructor
    public Person() {
        this.name = "Unknown";
        this.age = 0;
        this.email = "";
    }
    
    // Constructor with name
    public Person(String name) {
        this.name = name;
        this.age = 0;
        this.email = "";
    }
    
    // Constructor with name and age
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
        this.email = "";
    }
    
    // Constructor with all parameters
    public Person(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }
}

2. Runtime Polymorphism (Dynamic Polymorphism)

Resolved during runtime through method overriding and dynamic method dispatch.

Method Overriding

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

public class Circle extends Shape {
    private double radius;
    
    public Circle(double radius) {
        this.radius = radius;
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
    
    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }
}

public class Rectangle extends Shape {
    private double width, height;
    
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
    
    @Override
    public double calculateArea() {
        return width * height;
    }
}

// Polymorphic usage
Shape[] shapes = {
    new Circle(5.0),
    new Rectangle(4.0, 6.0),
    new Circle(3.0)
};

for (Shape shape : shapes) {
    shape.draw();                    // Calls appropriate draw method
    System.out.println("Area: " + shape.calculateArea()); // Calls appropriate area method
}

Dynamic Method Dispatch

Java uses dynamic method dispatch to determine which method to call at runtime:

public class Vehicle {
    public void start() {
        System.out.println("Vehicle starting...");
    }
    
    public void stop() {
        System.out.println("Vehicle stopping...");
    }
}

public class Car extends Vehicle {
    @Override
    public void start() {
        System.out.println("Car engine starting with ignition");
    }
    
    public void honk() {
        System.out.println("Car honking!");
    }
}

public class Motorcycle extends Vehicle {
    @Override
    public void start() {
        System.out.println("Motorcycle starting with kickstart");
    }
    
    public void wheelie() {
        System.out.println("Motorcycle doing a wheelie!");
    }
}

// Runtime polymorphism demonstration
public class VehicleDemo {
    public static void demonstratePolymorphism() {
        Vehicle vehicle1 = new Car();        // Upcasting
        Vehicle vehicle2 = new Motorcycle(); // Upcasting
        
        vehicle1.start(); // Calls Car's start method
        vehicle2.start(); // Calls Motorcycle's start method
        
        // vehicle1.honk();    // Compilation error - honk() not in Vehicle
        // vehicle2.wheelie(); // Compilation error - wheelie() not in Vehicle
        
        // Downcasting (with instanceof check)
        if (vehicle1 instanceof Car) {
            Car car = (Car) vehicle1;
            car.honk(); // Now we can call Car-specific methods
        }
        
        if (vehicle2 instanceof Motorcycle) {
            Motorcycle bike = (Motorcycle) vehicle2;
            bike.wheelie(); // Now we can call Motorcycle-specific methods
        }
    }
}

Polymorphism with Interfaces

Interfaces provide the ultimate form of polymorphism:

public interface Drawable {
    void draw();
    void resize(double factor);
}

public interface Colorable {
    void setColor(String color);
    String getColor();
}

// Multiple interface implementation
public class Circle implements Drawable, Colorable {
    private double radius;
    private String color;
    
    public Circle(double radius, String color) {
        this.radius = radius;
        this.color = color;
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing a " + color + " circle with radius " + radius);
    }
    
    @Override
    public void resize(double factor) {
        this.radius *= factor;
        System.out.println("Circle resized. New radius: " + radius);
    }
    
    @Override
    public void setColor(String color) {
        this.color = color;
    }
    
    @Override
    public String getColor() {
        return color;
    }
}

public class Square implements Drawable, Colorable {
    private double side;
    private String color;
    
    public Square(double side, String color) {
        this.side = side;
        this.color = color;
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing a " + color + " square with side " + side);
    }
    
    @Override
    public void resize(double factor) {
        this.side *= factor;
        System.out.println("Square resized. New side: " + side);
    }
    
    @Override
    public void setColor(String color) {
        this.color = color;
    }
    
    @Override
    public String getColor() {
        return color;
    }
}

// Using interface polymorphism
public class DrawingApp {
    public void processDrawables(Drawable[] drawables) {
        for (Drawable drawable : drawables) {
            drawable.draw();
            drawable.resize(1.5);
        }
    }
    
    public void processColorables(Colorable[] colorables) {
        for (Colorable colorable : colorables) {
            System.out.println("Current color: " + colorable.getColor());
            colorable.setColor("Blue");
        }
    }
}

Real-World Examples

1. Payment Processing System

public abstract class PaymentProcessor {
    protected double amount;
    
    public PaymentProcessor(double amount) {
        this.amount = amount;
    }
    
    public abstract boolean processPayment();
    public abstract String getPaymentMethod();
    
    public void generateReceipt() {
        System.out.println("Payment Receipt");
        System.out.println("Method: " + getPaymentMethod());
        System.out.println("Amount: $" + amount);
        System.out.println("Status: " + (processPayment() ? "Success" : "Failed"));
    }
}

public class CreditCardProcessor extends PaymentProcessor {
    private String cardNumber;
    private String cardHolder;
    
    public CreditCardProcessor(double amount, String cardNumber, String cardHolder) {
        super(amount);
        this.cardNumber = cardNumber;
        this.cardHolder = cardHolder;
    }
    
    @Override
    public boolean processPayment() {
        // Credit card processing logic
        System.out.println("Processing credit card payment...");
        return validateCard() && chargeCard();
    }
    
    @Override
    public String getPaymentMethod() {
        return "Credit Card";
    }
    
    private boolean validateCard() {
        // Validation logic
        return cardNumber.length() == 16;
    }
    
    private boolean chargeCard() {
        // Charging logic
        return amount <= 10000; // Example limit
    }
}

public class PayPalProcessor extends PaymentProcessor {
    private String email;
    
    public PayPalProcessor(double amount, String email) {
        super(amount);
        this.email = email;
    }
    
    @Override
    public boolean processPayment() {
        System.out.println("Processing PayPal payment...");
        return authenticateUser() && transferFunds();
    }
    
    @Override
    public String getPaymentMethod() {
        return "PayPal";
    }
    
    private boolean authenticateUser() {
        // Authentication logic
        return email.contains("@");
    }
    
    private boolean transferFunds() {
        // Transfer logic
        return amount <= 5000; // Example limit
    }
}

public class BankTransferProcessor extends PaymentProcessor {
    private String accountNumber;
    private String routingNumber;
    
    public BankTransferProcessor(double amount, String accountNumber, String routingNumber) {
        super(amount);
        this.accountNumber = accountNumber;
        this.routingNumber = routingNumber;
    }
    
    @Override
    public boolean processPayment() {
        System.out.println("Processing bank transfer...");
        return validateAccount() && initiateTransfer();
    }
    
    @Override
    public String getPaymentMethod() {
        return "Bank Transfer";
    }
    
    private boolean validateAccount() {
        // Account validation logic
        return accountNumber.length() >= 8 && routingNumber.length() == 9;
    }
    
    private boolean initiateTransfer() {
        // Transfer initiation logic
        return amount <= 50000; // Example limit
    }
}

// Usage
public class PaymentService {
    public void processPayments(PaymentProcessor[] processors) {
        for (PaymentProcessor processor : processors) {
            processor.generateReceipt(); // Polymorphic method calls
            System.out.println("---");
        }
    }
    
    public static void main(String[] args) {
        PaymentProcessor[] payments = {
            new CreditCardProcessor(100.0, "1234567890123456", "John Doe"),
            new PayPalProcessor(250.0, "[email protected]"),
            new BankTransferProcessor(1000.0, "12345678", "123456789")
        };
        
        PaymentService service = new PaymentService();
        service.processPayments(payments);
    }
}

2. Media Player System

public interface MediaPlayer {
    void play();
    void pause();
    void stop();
    void setVolume(int volume);
    String getMediaType();
}

public class AudioPlayer implements MediaPlayer {
    private String fileName;
    private int volume = 50;
    private boolean isPlaying = false;
    
    public AudioPlayer(String fileName) {
        this.fileName = fileName;
    }
    
    @Override
    public void play() {
        isPlaying = true;
        System.out.println("Playing audio: " + fileName + " at volume " + volume);
    }
    
    @Override
    public void pause() {
        isPlaying = false;
        System.out.println("Audio paused: " + fileName);
    }
    
    @Override
    public void stop() {
        isPlaying = false;
        System.out.println("Audio stopped: " + fileName);
    }
    
    @Override
    public void setVolume(int volume) {
        this.volume = Math.max(0, Math.min(100, volume));
        System.out.println("Audio volume set to: " + this.volume);
    }
    
    @Override
    public String getMediaType() {
        return "Audio";
    }
}

public class VideoPlayer implements MediaPlayer {
    private String fileName;
    private int volume = 50;
    private boolean isPlaying = false;
    private String resolution = "1080p";
    
    public VideoPlayer(String fileName) {
        this.fileName = fileName;
    }
    
    @Override
    public void play() {
        isPlaying = true;
        System.out.println("Playing video: " + fileName + " at " + resolution + 
                          ", volume " + volume);
    }
    
    @Override
    public void pause() {
        isPlaying = false;
        System.out.println("Video paused: " + fileName);
    }
    
    @Override
    public void stop() {
        isPlaying = false;
        System.out.println("Video stopped: " + fileName);
    }
    
    @Override
    public void setVolume(int volume) {
        this.volume = Math.max(0, Math.min(100, volume));
        System.out.println("Video volume set to: " + this.volume);
    }
    
    @Override
    public String getMediaType() {
        return "Video";
    }
    
    public void setResolution(String resolution) {
        this.resolution = resolution;
        System.out.println("Video resolution set to: " + resolution);
    }
}

public class PodcastPlayer implements MediaPlayer {
    private String episodeTitle;
    private int volume = 50;
    private boolean isPlaying = false;
    private double playbackSpeed = 1.0;
    
    public PodcastPlayer(String episodeTitle) {
        this.episodeTitle = episodeTitle;
    }
    
    @Override
    public void play() {
        isPlaying = true;
        System.out.println("Playing podcast: " + episodeTitle + 
                          " at " + playbackSpeed + "x speed, volume " + volume);
    }
    
    @Override
    public void pause() {
        isPlaying = false;
        System.out.println("Podcast paused: " + episodeTitle);
    }
    
    @Override
    public void stop() {
        isPlaying = false;
        System.out.println("Podcast stopped: " + episodeTitle);
    }
    
    @Override
    public void setVolume(int volume) {
        this.volume = Math.max(0, Math.min(100, volume));
        System.out.println("Podcast volume set to: " + this.volume);
    }
    
    @Override
    public String getMediaType() {
        return "Podcast";
    }
    
    public void setPlaybackSpeed(double speed) {
        this.playbackSpeed = speed;
        System.out.println("Playback speed set to: " + speed + "x");
    }
}

// Media player controller using polymorphism
public class MediaController {
    private List<MediaPlayer> playlist;
    
    public MediaController() {
        this.playlist = new ArrayList<>();
    }
    
    public void addMedia(MediaPlayer player) {
        playlist.add(player);
    }
    
    public void playAll() {
        for (MediaPlayer player : playlist) {
            System.out.println("\n--- Playing " + player.getMediaType() + " ---");
            player.setVolume(75);
            player.play();
        }
    }
    
    public void stopAll() {
        for (MediaPlayer player : playlist) {
            player.stop();
        }
    }
    
    public void controlSpecificFeatures() {
        for (MediaPlayer player : playlist) {
            // Using instanceof for specific features
            if (player instanceof VideoPlayer) {
                VideoPlayer video = (VideoPlayer) player;
                video.setResolution("4K");
            } else if (player instanceof PodcastPlayer) {
                PodcastPlayer podcast = (PodcastPlayer) player;
                podcast.setPlaybackSpeed(1.5);
            }
        }
    }
}

Benefits of Polymorphism

1. Code Reusability

// Generic method that works with any Shape
public class ShapeProcessor {
    public void processShapes(Shape[] shapes) {
        double totalArea = 0;
        
        for (Shape shape : shapes) {
            shape.draw();                    // Polymorphic call
            totalArea += shape.calculateArea(); // Polymorphic call
        }
        
        System.out.println("Total area: " + totalArea);
    }
}

2. Flexibility and Extensibility

// Easy to add new types without changing existing code
public class Triangle extends Shape {
    private double base, height;
    
    public Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }
    
    @Override
    public void draw() {
        System.out.println("Drawing a triangle");
    }
    
    @Override
    public double calculateArea() {
        return 0.5 * base * height;
    }
}

// Existing ShapeProcessor code works without modification
Shape[] shapes = {
    new Circle(5),
    new Rectangle(4, 6),
    new Triangle(3, 8)  // New type works seamlessly
};

3. Maintainability

Polymorphism allows you to modify implementations without affecting client code:

public interface DatabaseConnection {
    void connect();
    void disconnect();
    void executeQuery(String query);
}

// Can switch between different database implementations
public class ApplicationService {
    private DatabaseConnection dbConnection;
    
    public ApplicationService(DatabaseConnection connection) {
        this.dbConnection = connection;
    }
    
    public void performOperation() {
        dbConnection.connect();
        dbConnection.executeQuery("SELECT * FROM users");
        dbConnection.disconnect();
    }
}

Common Pitfalls

1. ClassCastException

Animal animal = new Dog();
// Cat cat = (Cat) animal; // ClassCastException at runtime

// Safe downcasting
if (animal instanceof Cat) {
    Cat cat = (Cat) animal;
    cat.purr();
} else {
    System.out.println("Animal is not a Cat");
}

2. Method Hiding vs. Overriding

public class Parent {
    public static void staticMethod() {
        System.out.println("Parent static method");
    }
    
    public void instanceMethod() {
        System.out.println("Parent instance method");
    }
}

public class Child extends Parent {
    public static void staticMethod() { // Method hiding, not overriding
        System.out.println("Child static method");
    }
    
    @Override
    public void instanceMethod() { // Method overriding
        System.out.println("Child instance method");
    }
}

Parent p = new Child();
p.staticMethod();   // Calls Parent's static method (method hiding)
p.instanceMethod(); // Calls Child's instance method (polymorphism)

Summary

Polymorphism is a powerful OOP concept that enables:

  • Compile-time polymorphism: Method and constructor overloading
  • Runtime polymorphism: Method overriding and dynamic dispatch
  • Interface polymorphism: Multiple implementations of the same interface
  • Code flexibility: Write generic code that works with multiple types
  • Extensibility: Add new types without modifying existing code

Key principles:

  • Use abstract classes and interfaces to define common contracts
  • Implement dynamic method dispatch for runtime flexibility
  • Apply proper downcasting with instanceof checks
  • Leverage polymorphism for clean, maintainable, and extensible code

Polymorphism, combined with inheritance and encapsulation, forms the foundation of robust object-oriented design patterns and architectures.