Compile-Time Polymorphism
Compile-time polymorphism (static binding) resolves method calls at compile time. The compiler determines which method to call based on the method signature.
Method Overloading
public class MathUtils {
// Same 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;
}
}
// Compiler resolves at compile time
MathUtils math = new MathUtils();
math.add(1, 2); // Calls int version
math.add(1.5, 2.5); // Calls double version
math.add(1, 2, 3); // Calls 3-param version
math.add("Hi", " There"); // Calls String version
Constructor Overloading
public class User {
private String name;
private String email;
private int age;
public User() {
this.name = "Anonymous";
}
public User(String name) {
this.name = name;
}
public User(String name, String email) {
this.name = name;
this.email = email;
}
public User(String name, String email, int age) {
this.name = name;
this.email = email;
this.age = age;
}
}
Operator Overloading (Some Languages)
# Python supports operator overloading
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other): # Overload + operator
return Vector(self.x + other.x, self.y + other.y)
def __repr__(self): # Overload string representation
return f"Vector({self.x}, {self.y})"
Benefits of Compile-Time Polymorphism
- Readability: Same operation name for different input types
- Convenience: Multiple ways to call the same function
- Performance: No runtime overhead (resolved at compile time)
- Type Safety: Compiler ensures correct parameter types
Runtime Polymorphism
Runtime polymorphism (dynamic binding) resolves method calls at runtime based on the actual object type, not the reference type.
Method Overriding
public interface PaymentProcessor {
PaymentResult processPayment(Money amount);
}
public class CreditCardProcessor implements PaymentProcessor {
@Override
public PaymentResult processPayment(Money amount) {
// Credit card logic
return new PaymentResult(true, "CC-" + UUID.randomUUID());
}
}
public class PayPalProcessor implements PaymentProcessor {
@Override
public PaymentResult processPayment(Money amount) {
// PayPal logic
return new PaymentResult(true, "PP-" + UUID.randomUUID());
}
}
// Runtime polymorphism
PaymentProcessor processor = getProcessor(); // Could be either
processor.processPayment(amount); // Resolved at runtime
Polymorphic Collections
List<Shape> shapes = new ArrayList<>();
shapes.add(new Circle(5));
shapes.add(new Rectangle(4, 6));
shapes.add(new Triangle(3, 4, 5));
// Same method call, different behavior at runtime
for (Shape shape : shapes) {
System.out.println("Area: " + shape.getArea()); // Polymorphic!
System.out.println("Perimeter: " + shape.getPerimeter());
}
Dynamic Method Dispatch
Shape shape = new Circle(5);
shape.getArea() // Runtime:
│
├── Reference type: Shape (compile-time check)
├── Object type: Circle (runtime type)
└── Method called: Circle.getArea() (dynamic dispatch)
Use Cases in System Design
1. Plugin Architecture:
public interface Plugin {
void execute(Request request);
}
// Plugins loaded at runtime
List<Plugin> plugins = loadPlugins();
for (Plugin plugin : plugins) {
plugin.execute(request); // Each plugin has different behavior
}
2. Strategy Pattern:
public interface SortStrategy {
void sort(int[] array);
}
// Different sorting strategies
SortStrategy strategy = getUserChoice();
strategy.sort(array); // Runtime determines algorithm
3. Factory Pattern:
public class NotificationFactory {
public static Notification create(NotificationType type) {
return switch (type) {
case EMAIL -> new EmailNotification();
case SMS -> new SMSNotification();
case PUSH -> new PushNotification();
};
}
}
Notification notification = NotificationFactory.create(type);
notification.send(message); // Polymorphic behavior
Use Cases
Polymorphism is everywhere in system design. Recognizing when to apply it is a key skill.
Polymorphism in Real Systems
┌─────────────────────────────────────────────────────┐
│ Notification System │
├─────────────────────────────────────────────────────┤
│ │
│ Notification (interface) │
│ ├── EmailNotification │
│ ├── SMSNotification │
│ ├── PushNotification │
│ └── SlackNotification │
│ │
│ User preferences determine which to use │
│ Same API: notification.send(message) │
└─────────────────────────────────────────────────────┘
Pattern Applications
| Pattern | Polymorphism Type | Use Case |
|---|---|---|
| Strategy | Runtime | Algorithm selection |
| Observer | Runtime | Event handling |
| Factory | Runtime | Object creation |
| Template Method | Runtime | Algorithm skeleton |
| Decorator | Runtime | Adding behavior |
| Adapter | Runtime | Interface conversion |
Designing for Polymorphism
1. Program to Interface:
// Good: Depends on interface
List<PaymentProcessor> processors = List.of(
new CreditCardProcessor(),
new PayPalProcessor()
);
// Bad: Depends on concrete class
List<CreditCardProcessor> processors = ...;
2. Open-Closed Principle:
// Open for extension: add new DiscountStrategy implementations
// Closed for modification: no changes to existing code
public interface DiscountStrategy {
Money calculateDiscount(Order order);
}
public class PercentageDiscount implements DiscountStrategy { }
public class FixedAmountDiscount implements DiscountStrategy { }
public class BuyOneGetOneFree implements DiscountStrategy { }
3. Dependency Injection:
public class OrderService {
private final PaymentProcessor processor;
// Injected: runtime polymorphism
public OrderService(PaymentProcessor processor) {
this.processor = processor;
}
public Order checkout(Cart cart) {
return processor.processPayment(cart.getTotal());
}
}
When to Use Polymorphism
| Situation | Approach |
|---|---|
| Multiple implementations of same behavior | Runtime polymorphism |
| Algorithm selection at runtime | Strategy pattern |
| Same operation, different input types | Compile-time (overloading) |
| Plugin/extension architecture | Runtime polymorphism |
| Framework designed for extension | Runtime polymorphism |
Common Anti-Patterns
- Type checking with instanceof: Replace with polymorphism
- Switch on type: Use strategy/factory instead
- Downcasting: Indicates poor hierarchy design
- Leaking concrete types: Always return interfaces
Practice Problems
Design a scalable Polymorphism system. Cover high-level architecture, data model, and API design.
Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliabilityHow would you scale Polymorphism to handle 10x the current load? Identify bottlenecks and solutions.
Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decompositionAnalyze potential failure modes for Polymorphism and design mitigation strategies.
Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradationQuiz
1. What is the key difference between compile-time and runtime polymorphism?
2. Which is an example of runtime polymorphism?
3. What pattern uses runtime polymorphism for algorithm selection?
4. Why should you program to interfaces in polymorphic designs?
5. What does 'dynamic method dispatch' mean?
Flashcards
Question
What is compile-time polymorphism?
Click to reveal answer
Answer
Method overloading resolved at compile time. Same method name with different parameter lists. The compiler determines which method to call based on the method signature.
Question
What is runtime polymorphism?
Click to reveal answer
Answer
Method overriding where the JVM determines which method to call at runtime based on the actual object type. Enables substituting different implementations through a common interface.
Question
What is dynamic method dispatch?
Click to reveal answer
Answer
The runtime mechanism where the JVM selects the appropriate overridden method based on the actual object type, not the reference type. Key to runtime polymorphism.
Question
Name 3 design patterns that use runtime polymorphism.
Click to reveal answer
Answer
Strategy (algorithm selection), Observer (event handling), Factory (object creation), Decorator (adding behavior), Adapter (interface conversion).
Question
Why program to interfaces in polymorphic designs?
Click to reveal answer
Answer
Enables swapping implementations without changing consumer code. Provides loose coupling and flexibility. Code depends on contracts, not implementations.
Revision Notes
Key Takeaways
- 1.Compile-time polymorphism uses method overloading; runtime uses method overriding
- 2.Runtime polymorphism enables substituting implementations through common interfaces
- 3.Design patterns like Strategy, Observer, and Factory leverage runtime polymorphism
- 4.Always program to interfaces for maximum flexibility
- 5.Dynamic dispatch determines the correct method at runtime based on object type
Interview Tips
- •Explain both types of polymorphism when asked about OOP concepts
- •Show how polymorphism enables the Strategy pattern in your design
- •Discuss dynamic dispatch when explaining why overridden methods are called
- •Mention programming to interfaces as a key polymorphism application
Cheat Sheet
Polymorphism - Cheat Sheet
Compile-Time (Static):
- Method overloading
- Resolved at compile time
- Same name, different parameters
- No runtime overhead
Runtime (Dynamic):
- Method overriding
- Resolved at runtime
- Same signature, different implementations
- Based on actual object type
Use Cases:
| Pattern | Type |
|---|---|
| Strategy | Runtime |
| Observer | Runtime |
| Factory | Runtime |
| Decorator | Runtime |
Design for Polymorphism:
- Program to interface
- Follow Open-Closed Principle
- Use dependency injection
- Return interfaces, not concrete types
Anti-Patterns:
- Switch on type → Use strategy
- instanceof checks → Use polymorphism
- Downcasting → Poor hierarchy design