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
-
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; } }
-
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:
-
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 } }
-
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
-
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)); } }
-
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
-
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 } }
-
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
-
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(); } } }
-
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
-
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<>();
-
Program to an Interface
public class DataProcessor { private final DataSource dataSource; public DataProcessor(DataSource dataSource) { this.dataSource = dataSource; } public void process() { dataSource.getData(); } }
-
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
-
Static Method Hiding
public class StaticExample { public static void main(String[] args) { Parent parent = new Child(); parent.staticMethod(); // Calls Parent's method } }
-
Private Method Overriding
public class PrivateExample { class Parent { private void method() {} } class Child extends Parent { private void method() {} // Not an override } }
Performance Considerations
-
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 } }
-
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(); } } }