Polymorphism: One Interface, Many Forms

Duration: 45 min

Polymorphism: One Interface, Many Forms

Duration: 45 min

Introduction

Polymorphism means "many forms." It allows objects of different types to be used interchangeably through a common interface. A method call on a parent type can trigger different implementations in different child types. This is the most powerful feature of OOP—it enables flexible, extensible code that works with types you haven't written yet.

This module covers runtime polymorphism (dynamic dispatch), method overloading, interfaces, type checking with instanceof, and patterns for leveraging polymorphism effectively.

Runtime Polymorphism (Dynamic Dispatch)

Java determines which method to call at runtime based on the actual object type, not the reference type. This enables writing flexible code.

public class Payment {
    public void process(double amount) {
        System.out.println("Processing payment: $" + amount);
    }
}

public class CreditCardPayment extends Payment { @Override public void process(double amount) { System.out.println("Processing credit card payment: $" + amount); } }

public class PayPalPayment extends Payment { @Override public void process(double amount) { System.out.println("Processing PayPal payment: $" + amount); } }

public class CheckPayment extends Payment { @Override public void process(double amount) { System.out.println("Processing check payment: $" + amount); } }

public class PaymentProcessor { // Method accepts Payment type - works with any subclass public void processTransaction(Payment payment, double amount) { payment.process(amount); // Which method runs depends on actual type } }

public class PolymorphismDemo { public static void main(String[] args) { PaymentProcessor processor = new PaymentProcessor(); // Each call dispatches to different implementation processor.processTransaction(new CreditCardPayment(), 100); processor.processTransaction(new PayPalPayment(), 50); processor.processTransaction(new CheckPayment(), 75); } }

The processor doesn't need to know about every payment type. It works with any Payment subclass. If you add a new payment type later, the processor works without modification. This is the Open-Closed Principle: open for extension, closed for modification.

Method Overloading

Overloading creates multiple methods with the same name but different parameters. The compiler chooses which version to call based on argument types (compile-time decision).

public class Calculator {
    // Overload 1: two integers
    public int add(int a, int b) {
        return a + b;
    }
    
    // Overload 2: two doubles
    public double add(double a, double b) {
        return a + b;
    }
    
    // Overload 3: three integers
    public int add(int a, int b, int c) {
        return a + b + c;
    }
    
    // Overload 4: variable arguments
    public int add(int... numbers) {
        int sum = 0;
        for (int n : numbers) {
            sum += n;
        }
        return sum;
    }
    
    // Overload 5: String and int
    public String add(String prefix, int value) {
        return prefix + ": " + value;
    }
}

public class OverloadingDemo { public static void main(String[] args) { Calculator calc = new Calculator(); System.out.println(calc.add(5, 3)); // 8 (int version) System.out.println(calc.add(5.5, 3.2)); // 8.7 (double version) System.out.println(calc.add(1, 2, 3)); // 6 (three arg version) System.out.println(calc.add(1, 2, 3, 4, 5)); // 15 (varargs version) System.out.println(calc.add("Total", 42)); // Total: 42 (String version) } }

Overloading differs from overriding: overloading is compile-time (static), overriding is runtime (dynamic). Method signatures must differ in parameter count or type. Return type alone doesn't distinguish overloads.

Interfaces

Interfaces define contracts without providing implementation. Classes implement interfaces to provide concrete behavior.

public interface DataSource {
    void connect();
    void disconnect();
    String fetchData();
}

public class DatabaseConnection implements DataSource { @Override public void connect() { System.out.println("Connecting to database..."); } @Override public void disconnect() { System.out.println("Disconnecting from database"); } @Override public String fetchData() { return "Data from database"; } }

public class ApiConnection implements DataSource { @Override public void connect() { System.out.println("Connecting to API..."); } @Override public void disconnect() { System.out.println("Disconnecting from API"); } @Override public String fetchData() { return "Data from API"; } }

public class FileConnection implements DataSource { @Override public void connect() { System.out.println("Opening file..."); } @Override public void disconnect() { System.out.println("Closing file"); } @Override public String fetchData() { return "Data from file"; } }

public class DataProcessor { public void process(DataSource source) { source.connect(); String data = source.fetchData(); System.out.println("Processing: " + data); source.disconnect(); } }

public class InterfaceDemo { public static void main(String[] args) { DataProcessor processor = new DataProcessor(); processor.process(new DatabaseConnection()); System.out.println(); processor.process(new ApiConnection()); System.out.println(); processor.process(new FileConnection()); } }

Interfaces are more flexible than abstract classes—a class can implement multiple interfaces but extend only one class. Use interfaces to define contracts that unrelated classes should follow.

Instanceof Operator

Check an object's actual type at runtime with instanceof.

public class Shape {
    public void describe() {
        System.out.println("This is a shape");
    }
}

public class Circle extends Shape { private double radius; public Circle(double radius) { this.radius = radius; } @Override public void describe() { System.out.println("Circle with radius: " + radius); } public double getArea() { 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 describe() { System.out.println("Rectangle: " + width + "x" + height); } public double getArea() { return width * height; } }

public class ShapeProcessor { public void analyzeShape(Shape shape) { shape.describe(); // Check actual type and call specific methods if (shape instanceof Circle) { Circle circle = (Circle) shape; // Cast to Circle System.out.println("Area: " + circle.getArea()); } else if (shape instanceof Rectangle) { Rectangle rect = (Rectangle) shape; // Cast to Rectangle System.out.println("Area: " + rect.getArea()); } } }

public class InstanceofDemo { public static void main(String[] args) { ShapeProcessor processor = new ShapeProcessor(); processor.analyzeShape(new Circle(5)); System.out.println(); processor.analyzeShape(new Rectangle(4, 6)); } }

instanceof checks if an object is an instance of a class (or any parent). Use it before casting to avoid ClassCastException. In Java 14+, you can use pattern matching: if (shape instanceof Circle circle) { ... }.

Type Casting

Casting converts a reference from one type to another. Upcasting (to parent) is always safe; downcasting (to child) requires instanceof check.

public class Animal {
    public void makeSound() {
        System.out.println("Generic sound");
    }
}

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

public class CastingDemo { public static void main(String[] args) { // Upcasting - always safe Animal animal = new Dog(); // Implicit cast to parent animal.makeSound(); // Woof! // animal.fetch(); // ERROR: Animal doesn't have fetch() // Downcasting - requires explicit cast if (animal instanceof Dog) { Dog dog = (Dog) animal; // Explicit downcast dog.fetch(); } // This throws ClassCastException Animal cat = new Animal(); if (cat instanceof Dog) { Dog dog = (Dog) cat; // Won't run - cat is not a Dog } else { System.out.println("cat is not a Dog"); } } }

Upcasting is implicit and safe—you're moving to a more general type. Downcasting is explicit and risky—the cast could fail at runtime. Always check with instanceof before downcasting.

Common Polymorphic Patterns

Pattern 1: Strategy Pattern

interface SortStrategy {
    void sort(int[] array);
}

class BubbleSort implements SortStrategy { public void sort(int[] array) { // Bubble sort implementation } }

class QuickSort implements SortStrategy { public void sort(int[] array) { // Quick sort implementation } }

class Sorter { private SortStrategy strategy; public Sorter(SortStrategy strategy) { this.strategy = strategy; } public void executeSort(int[] array) { strategy.sort(array); // Polymorphic call } }

Pattern 2: Collections with Polymorphism

// Process different shape types uniformly
ArrayList shapes = new ArrayList<>();
shapes.add(new Circle(5));
shapes.add(new Rectangle(4, 6));
shapes.add(new Triangle(3, 4));

for (Shape shape : shapes) { shape.describe(); // Calls appropriate method }

Adapter and Bridge Patterns

Adapter Pattern for Polymorphism

public class AdapterPattern {
    // Old interface
    interface OldFileReader {
        String read();
    }
    
    // New interface
    interface NewDataLoader {
        java.util.List loadLines();
    }
    
    // Adapter to make OldFileReader work as NewDataLoader
    static class FileReaderAdapter implements NewDataLoader {
        private OldFileReader oldReader;
        
        public FileReaderAdapter(OldFileReader oldReader) {
            this.oldReader = oldReader;
        }
        
        @Override
        public java.util.List loadLines() {
            String content = oldReader.read();
            java.util.List lines = new java.util.ArrayList<>();
            for (String line : content.split("\n")) {
                lines.add(line);
            }
            return lines;
        }
    }
}

Deep Dive: Method Resolution Order

public class MethodResolution {
    interface Printable {
        default void print() {
            System.out.println("Printable");
        }
    }
    
    interface Drawable {
        default void print() {
            System.out.println("Drawable");
        }
    }
    
    // Implementing multiple interfaces
    static class Document implements Printable, Drawable {
        @Override
        public void print() {
            // Must explicitly choose which to call
            Printable.super.print();  // Or Drawable.super.print()
        }
    }
    
    // With inheritance
    static class BasePrintable {
        public void print() {
            System.out.println("Base");
        }
    }
    
    interface IPrintable {
        void print();
    }
    
    static class Document2 extends BasePrintable implements IPrintable {
        // Inheritance takes precedence over interface
        public void print() {
            super.print();  // Calls BasePrintable.print()
        }
    }
}

Polymorphic Collections

public class PolymorphicCollections {
    interface Logger {
        void log(String message);
    }
    
    static class ConsoleLogger implements Logger {
        @Override
        public void log(String message) {
            System.out.println("[CONSOLE] " + message);
        }
    }
    
    static class FileLogger implements Logger {
        @Override
        public void log(String message) {
            System.out.println("[FILE] " + message);
        }
    }
    
    static class EmailLogger implements Logger {
        @Override
        public void log(String message) {
            System.out.println("[EMAIL] " + message);
        }
    }
    
    static class Application {
        private java.util.List loggers = new java.util.ArrayList<>();
        
        public void addLogger(Logger logger) {
            loggers.add(logger);
        }
        
        public void notifyAll(String message) {
            for (Logger logger : loggers) {
                logger.log(message);  // Polymorphic call
            }
        }
    }
    
    public static void main(String[] args) {
        Application app = new Application();
        app.addLogger(new ConsoleLogger());
        app.addLogger(new FileLogger());
        app.addLogger(new EmailLogger());
        
        app.notifyAll("System started");
        // All loggers log the message differently
    }
}

Generics and Polymorphism

import java.util.List;

public class GenericsAndPolymorphism { interface Container { void store(T item); T retrieve(); } static class StringContainer implements Container { private String content; @Override public void store(String item) { this.content = item; } @Override public String retrieve() { return content; } } static class IntegerContainer implements Container { private Integer content; @Override public void store(Integer item) { this.content = item; } @Override public Integer retrieve() { return content; } } // Works with any Container type static void processContainer(Container container, T value) { container.store(value); T retrieved = container.retrieve(); System.out.println("Stored and retrieved: " + retrieved); } public static void main(String[] args) { processContainer(new StringContainer(), "Hello"); processContainer(new IntegerContainer(), 42); } }

Real-World Application: Payment Gateway

public class PaymentGateway {
    interface PaymentMethod {
        boolean validate();
        boolean processPayment(double amount);
        String getDetails();
    }
    
    static class CreditCard implements PaymentMethod {
        private String cardNumber, cvv;
        
        @Override
        public boolean validate() {
            return cardNumber.length() == 16;
        }
        
        @Override
        public boolean processPayment(double amount) {
            System.out.println("Processing $" + amount + " via Credit Card");
            return true;
        }
        
        @Override
        public String getDetails() {
            return "Credit Card ending in " + cardNumber.substring(12);
        }
    }
    
    static class PayPal implements PaymentMethod {
        private String email;
        
        @Override
        public boolean validate() {
            return email.contains("@");
        }
        
        @Override
        public boolean processPayment(double amount) {
            System.out.println("Processing $" + amount + " via PayPal");
            return true;
        }
        
        @Override
        public String getDetails() {
            return "PayPal account: " + email;
        }
    }
    
    static class ApplePay implements PaymentMethod {
        private String deviceId;
        
        @Override
        public boolean validate() {
            return !deviceId.isEmpty();
        }
        
        @Override
        public boolean processPayment(double amount) {
            System.out.println("Processing $" + amount + " via Apple Pay");
            return true;
        }
        
        @Override
        public String getDetails() {
            return "Apple Pay on device " + deviceId;
        }
    }
    
    static class CheckoutProcessor {
        public void checkout(PaymentMethod method, double amount) {
            if (!method.validate()) {
                System.out.println("Payment method invalid");
                return;
            }
            
            if (method.processPayment(amount)) {
                System.out.println("Payment successful: " + method.getDetails());
            } else {
                System.out.println("Payment failed");
            }
        }
    }
}

Key Takeaways

1. Runtime polymorphism dispatches method calls based on actual object type 2. Method overloading allows multiple methods with same name but different signatures 3. Interfaces define contracts that unrelated classes can implement 4. instanceof checks object type before downcasting 5. Upcasting is implicit; downcasting requires explicit cast and instanceof check 6. Polymorphism enables writing code that works with unknown future types 7. Strategy pattern uses polymorphism for flexible algorithms 8. Adapter pattern provides compatibility between different interfaces 9. Polymorphic collections can store objects of different types uniformly 10. Generics combined with polymorphism provides type-safe flexibility 11. Default interface methods provide backward compatibility 12. Method resolution with inheritance follows predictable rules

Quiz

Question 1: What happens with this code?

Animal a = new Dog();
a.makeSound();
  • A) Calls Animal's makeSound
  • B) Calls Dog's makeSound ✓
  • C) Compilation error
  • D) Runtime error

Question 2: Which is true about method overloading?

  • A) Must have different return types
  • B) Must have different parameter types or count ✓
  • C) Must be in different classes
  • D) Is resolved at runtime

Question 3: When should you use instanceof?

  • A) Before downcasting ✓
  • B) Before upcasting
  • C) Always when casting
  • D) Never in production code

Question 4: Can a class implement multiple interfaces?

  • A) No, only one
  • B) Yes, multiple ✓
  • C) Only if using extends
  • D) Only in Java 17+

Question 5: What's the difference between overloading and overriding?

  • A) Overloading is runtime; overriding is compile-time
  • B) Overloading is compile-time; overriding is runtime ✓
  • C) Same thing
  • D) Overriding uses interfaces

Question 6: Which cast is always safe?

  • A) Downcasting to child
  • B) Upcasting to parent ✓
  • C) Both equally safe
  • D) Neither is safe