Skip to main content

Polymorphism in Java

Core Understanding

Polymorphism allows objects to take multiple forms, enabling:

  • Runtime method selection based on actual object type
  • Compile-time method selection based on parameters
  • Interface-based programming
  • Behavior customization without changing calling code
  • Flexible and extensible designs

Types of Polymorphism:

  • Runtime (Dynamic) Polymorphism: Method overriding
  • Compile-time (Static) Polymorphism: Method overloading
  • Parametric Polymorphism: Generics
  • Ad-hoc Polymorphism: Method overloading

❌ Bad Example

public class PaymentProcessor {
public void processPayment(String paymentType, double amount) {
if (paymentType.equals("CREDIT_CARD")) {
processCreditCardPayment(amount);
} else if (paymentType.equals("PAYPAL")) {
processPayPalPayment(amount);
} else if (paymentType.equals("CRYPTO")) {
processCryptoPayment(amount);
}
}

private void processCreditCardPayment(double amount) {
// Credit card specific logic
}

private void processPayPalPayment(double amount) {
// PayPal specific logic
}

private void processCryptoPayment(double amount) {
// Crypto specific logic
}
}

// Usage
processor.processPayment("CREDIT_CARD", 100.00);

Why it's bad:

  • Switch/if-else based on type
  • Hard to extend with new payment types
  • Violates Open-Closed Principle
  • Type safety issues
  • Duplicate code structure

✅ Good Example

Let's fix this:

public interface PaymentProcessor {
PaymentResult process(Payment payment);
boolean supports(PaymentMethod method);
}

@Service
public class CreditCardProcessor implements PaymentProcessor {
private final CreditCardGateway gateway;
private final TransactionLogger logger;

@Override
public PaymentResult process(Payment payment) {
logger.logAttempt(payment);

try {
CreditCardDetails details = payment.getDetails();
TransactionResult result = gateway.charge(details, payment.getAmount());

logger.logResult(result);
return PaymentResult.from(result);
} catch (GatewayException e) {
return PaymentResult.failure(e.getMessage());
}
}

@Override
public boolean supports(PaymentMethod method) {
return PaymentMethod.CREDIT_CARD.equals(method);
}
}

@Service
public class PaymentService {
private final List<PaymentProcessor> processors;

public PaymentResult processPayment(Payment payment) {
return processors.stream()
.filter(processor -> processor.supports(payment.getMethod()))
.findFirst()
.orElseThrow(() -> new UnsupportedPaymentMethodException(payment.getMethod()))
.process(payment);
}
}

Why it's good:

  • Type-safe
  • Easy to extend
  • Clear separation of concerns
  • Follows Open-Closed Principle
  • Runtime polymorphism through interfaces

Best Practices

  • Use Interface-based Programming
public interface NotificationSender {
void send(Notification notification);
}

@Service
public class NotificationService {
private final Map<NotificationType, NotificationSender> senders;

public void notify(Notification notification) {
senders.get(notification.getType()).send(notification);
}
}
  • Leverage Generics for Type Safety
public interface Repository<T, ID> {
Optional<T> findById(ID id);
T save(T entity);
void delete(ID id);
}

public class JpaUserRepository implements Repository<User, Long> {
@Override
public Optional<User> findById(Long id) {
// Implementation
}
}
  • Use Method Overloading Judiciously
public class EmailBuilder {
public EmailBuilder withSubject(String subject) {
// Set subject
return this;
}

public EmailBuilder withTemplate(String template, Map<String, Object> params) {
// Set template with parameters
return this;
}

public EmailBuilder withTemplate(String template) {
// Set template without parameters
return this;
}
}

Use Cases

  • Plugin Systems
    • Dynamic loading of implementations
    • Feature extensions
    • Custom handlers
  • Strategy Pattern
    • Payment processing
    • Sorting algorithms
    • Validation strategies
  • Event Handling
    • GUI event listeners
    • Message processors
    • Event-driven systems

Anti-patterns to Avoid

  • Type Checking with instanceof
// Avoid
public void process(Object obj) {
if (obj instanceof String) {
// Handle String
} else if (obj instanceof Integer) {
// Handle Integer
}
}

// Better
public interface Processable {
void process();
}
  • Overloading with Similar Parameters
// Avoid confusing overloads
public void save(String data) { }
public void save(String info) { } // Confusing!

// Better
public void saveData(String data) { }
public void saveInfo(String info) { }
  • Breaking Method Contract in Overrides
class Parent {
public List<String> process() { return new ArrayList<>(); }
}

class Child extends Parent {
@Override
public List<String> process() {
return null; // Breaking contract!
}
}