Skip to content

Polymorphism and Dynamic Dispatch in Java

Question

Explain the concept of polymorphism in Java, focusing on dynamic dispatch. How does it work, and what are its key benefits and implementation details?

Answer

Polymorphism is one of the core principles of Object-Oriented Programming that allows objects to take multiple forms. In Java, polymorphism is primarily achieved through inheritance and interfaces, with dynamic dispatch being the mechanism that enables runtime polymorphism.

Types of Polymorphism

  1. Compile-time Polymorphism (Method Overloading)

    public class Calculator {
        public int add(int a, int b) {
            return a + b;
        }
    
        public double add(double a, double b) {
            return a + b;
        }
    }
    

  2. Runtime Polymorphism (Method Overriding)

    public class Animal {
        public void makeSound() {
            System.out.println("Some sound");
        }
    }
    
    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!");
        }
    }
    

Dynamic Dispatch

Dynamic dispatch is the mechanism by which a call to an overridden method is resolved at runtime rather than compile time.

How Dynamic Dispatch Works:

  1. Method Table (Virtual Method Table)

    public class Example {
        public static void main(String[] args) {
            Animal animal = new Dog();  // Reference type: Animal, Actual type: Dog
            animal.makeSound();  // Calls Dog's makeSound() at runtime
    
            animal = new Cat();  // Same reference, different actual type
            animal.makeSound();  // Calls Cat's makeSound() at runtime
        }
    }
    

  2. Runtime Type Information (RTTI)

    public class RTTIExample {
        public static void main(String[] args) {
            Animal animal = new Dog();
            System.out.println(animal instanceof Dog);  // true
            System.out.println(animal.getClass());      // class Dog
        }
    }
    

Benefits of Polymorphism

  1. Code Reusability

    public class ShapeProcessor {
        public void processShape(Shape shape) {
            double area = shape.calculateArea();
            double perimeter = shape.calculatePerimeter();
            // Process shape regardless of its specific type
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            ShapeProcessor processor = new ShapeProcessor();
            processor.processShape(new Circle(5));
            processor.processShape(new Rectangle(4, 6));
            processor.processShape(new Triangle(3, 4, 5));
        }
    }
    

  2. Interface-based Programming

    public interface PaymentMethod {
        void processPayment(double amount);
    }
    
    public class PaymentProcessor {
        public void process(PaymentMethod method, double amount) {
            method.processPayment(amount);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            PaymentProcessor processor = new PaymentProcessor();
            processor.process(new CreditCard(), 100.0);
            processor.process(new PayPal(), 100.0);
            processor.process(new Bitcoin(), 100.0);
        }
    }
    

Implementation Details

  1. Method Resolution

    public class MethodResolution {
        public static void main(String[] args) {
            Parent parent = new Child();
            parent.method();  // Calls Child's method
    
            // Static methods are not overridden
            parent.staticMethod();  // Calls Parent's static method
        }
    }
    

  2. Covariant Return Types

    public class CovariantExample {
        class Parent {
            public Number getValue() {
                return 1;
            }
        }
    
        class Child extends Parent {
            @Override
            public Integer getValue() {  // Covariant return type
                return 1;
            }
        }
    }
    

Common Use Cases

  1. Plugin Architecture

    public interface Plugin {
        void initialize();
        void execute();
        void cleanup();
    }
    
    public class PluginManager {
        private List<Plugin> plugins = new ArrayList<>();
    
        public void addPlugin(Plugin plugin) {
            plugins.add(plugin);
        }
    
        public void executeAll() {
            for (Plugin plugin : plugins) {
                plugin.execute();
            }
        }
    }
    

  2. Strategy Pattern

    public interface SortingStrategy {
        void sort(int[] array);
    }
    
    public class Sorter {
        private SortingStrategy strategy;
    
        public void setStrategy(SortingStrategy strategy) {
            this.strategy = strategy;
        }
    
        public void sort(int[] array) {
            strategy.sort(array);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Sorter sorter = new Sorter();
            sorter.setStrategy(new QuickSort());
            sorter.sort(array);
    
            sorter.setStrategy(new MergeSort());
            sorter.sort(array);
        }
    }
    

Best Practices

  1. Use Interface Types

    // Good
    List<String> list = new ArrayList<>();
    Map<String, Integer> map = new HashMap<>();
    
    // Bad
    ArrayList<String> list = new ArrayList<>();
    HashMap<String, Integer> map = new HashMap<>();
    

  2. Program to an Interface

    public class DataProcessor {
        private final DataSource dataSource;
    
        public DataProcessor(DataSource dataSource) {
            this.dataSource = dataSource;
        }
    
        public void process() {
            dataSource.getData();
        }
    }
    

  3. Use instanceof Judiciously

    // Good
    if (shape instanceof Circle) {
        Circle circle = (Circle) shape;
        double radius = circle.getRadius();
    }
    
    // Better - Use pattern matching (Java 14+)
    if (shape instanceof Circle circle) {
        double radius = circle.getRadius();
    }
    

Common Pitfalls

  1. Static Method Hiding

    public class StaticExample {
        public static void main(String[] args) {
            Parent parent = new Child();
            parent.staticMethod();  // Calls Parent's method
        }
    }
    

  2. Private Method Overriding

    public class PrivateExample {
        class Parent {
            private void method() {}
        }
    
        class Child extends Parent {
            private void method() {}  // Not an override
        }
    }
    

Performance Considerations

  1. Method Dispatch Overhead

    public class PerformanceExample {
        // Virtual method call
        public void virtualCall(Animal animal) {
            animal.makeSound();  // Runtime dispatch
        }
    
        // Direct method call
        public void directCall(Dog dog) {
            dog.makeSound();  // Compile-time dispatch
        }
    }
    

  2. JIT Optimization

    public class JITExample {
        public static void main(String[] args) {
            Animal animal = new Dog();
            // After JIT optimization, this might be inlined
            for (int i = 0; i < 1000; i++) {
                animal.makeSound();
            }
        }
    }