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.