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