Java Polymorphism - Complete Tutorial
Master Java Polymorphism: Learn method overloading, method overriding, runtime vs compile-time polymorphism, inheritance, interfaces, abstract classes, and real-world examples.
Many Forms
Single interface, multiple forms
Method Overloading
Compile-time polymorphism
Method Overriding
Runtime polymorphism
Dynamic Dispatch
JVM decides method at runtime
1. Introduction to Polymorphism
Polymorphism is a core concept in Object-Oriented Programming (OOP) that allows objects of different types to be treated as objects of a common super type. The word "polymorphism" comes from Greek words "poly" (many) and "morph" (forms), meaning "many forms".
Key Characteristics
- Single Interface: One interface, multiple implementations
- Flexibility: Code works with objects of different types
- Extensibility: Easy to add new types without modifying existing code
- Code Reusability: Common interfaces reduce code duplication
- Maintainability: Easier to maintain and extend systems
- Type Safety: Compile-time checking of method calls
Real-World Examples
- GUI Components: Button, TextField, Checkbox - all are Components
- Payment Methods: CreditCard, PayPal, Bitcoin - all are Payment
- Animal Sounds: Dog.bark(), Cat.meow(), Bird.chirp()
- Database Connections: MySQL, PostgreSQL, Oracle - all are Connection
- File Formats: PDF, DOCX, TXT - all are Document
- Vehicle Types: Car.drive(), Plane.fly(), Boat.sail()
The Power of Polymorphism
Polymorphism enables you to write flexible and extensible code. Instead of writing separate methods for each object type, you can write one method that works with the super type, and let Java handle the specific implementation at runtime.
// Base class
abstract class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
// Abstract method - must be implemented by subclasses
public abstract void makeSound();
// Concrete method - common to all animals
public void eat() {
System.out.println(name + " is eating.");
}
}
// Derived classes
class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(name + " says: Woof! Woof!");
}
// Additional method specific to Dog
public void wagTail() {
System.out.println(name + " is wagging its tail.");
}
}
class Cat extends Animal {
public Cat(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(name + " says: Meow!");
}
// Additional method specific to Cat
public void climbTree() {
System.out.println(name + " is climbing a tree.");
}
}
class Bird extends Animal {
public Bird(String name) {
super(name);
}
@Override
public void makeSound() {
System.out.println(name + " says: Chirp! Chirp!");
}
// Additional method specific to Bird
public void fly() {
System.out.println(name + " is flying.");
}
}
public class PolymorphismDemo {
public static void main(String[] args) {
System.out.println("=== Polymorphism Demonstration ===\n");
// Creating objects of different types
Animal myDog = new Dog("Buddy");
Animal myCat = new Cat("Whiskers");
Animal myBird = new Bird("Tweety");
// Array of Animal references
Animal[] animals = {myDog, myCat, myBird};
// Polymorphic method calls
System.out.println("=== Making Sounds (Polymorphic Behavior) ===");
for (Animal animal : animals) {
animal.makeSound(); // Each animal makes its specific sound
}
System.out.println("\n=== Eating (Common Behavior) ===");
for (Animal animal : animals) {
animal.eat(); // All animals can eat (inherited method)
}
System.out.println("\n=== Type Checking and Casting ===");
for (Animal animal : animals) {
// Check the actual type
if (animal instanceof Dog) {
Dog dog = (Dog) animal; // Safe casting
dog.wagTail(); // Call Dog-specific method
} else if (animal instanceof Cat) {
Cat cat = (Cat) animal; // Safe casting
cat.climbTree(); // Call Cat-specific method
} else if (animal instanceof Bird) {
Bird bird = (Bird) animal; // Safe casting
bird.fly(); // Call Bird-specific method
}
}
System.out.println("\n=== Benefits of Polymorphism ===");
System.out.println("1. Code reusability: One method works for all Animal types");
System.out.println("2. Extensibility: Easy to add new Animal types");
System.out.println("3. Maintainability: Changes in one place affect all types");
System.out.println("4. Flexibility: Objects can be treated generically");
}
}
2. Types of Polymorphism in Java
Java supports two main types of polymorphism: Compile-time Polymorphism (Static) and Runtime Polymorphism (Dynamic).
Compile-Time Polymorphism (Static)
- Method Overloading: Multiple methods with same name, different parameters
- Operator Overloading: Not supported in Java (except + for strings)
- Resolution: At compile time by compiler
- Also Known As: Static binding, early binding
- Key Feature: Method signature must differ
- Example: System.out.println() - multiple versions
Runtime Polymorphism (Dynamic)
- Method Overriding: Subclass provides specific implementation
- Dynamic Method Dispatch: JVM decides which method to call
- Resolution: At runtime by JVM
- Also Known As: Dynamic binding, late binding
- Key Feature: Requires inheritance
- Example: Animal.makeSound() - different for each animal
Compile-Time vs Runtime Polymorphism
| Aspect | Compile-Time Polymorphism | Runtime Polymorphism |
|---|---|---|
| Type | Static / Early Binding | Dynamic / Late Binding |
| Mechanism | Method Overloading | Method Overriding |
| Resolution Time | Compile time | Runtime |
| Performance | Faster (no runtime overhead) | Slower (runtime decision) |
| Inheritance | Not required | Required |
| Flexibility | Less flexible | More flexible |
| Example | println(int), println(String) | Animal.sound() for Dog, Cat |
| Key Advantage | Code clarity, multiple options | Extensibility, code reuse |
public class PolymorphismTypes {
// === COMPILE-TIME POLYMORPHISM (Method Overloading) ===
// Different versions of add method
public int add(int a, int b) {
System.out.println("Adding two integers: " + a + " + " + b);
return a + b;
}
public double add(double a, double b) {
System.out.println("Adding two doubles: " + a + " + " + b);
return a + b;
}
public int add(int a, int b, int c) {
System.out.println("Adding three integers: " + a + " + " + b + " + " + c);
return a + b + c;
}
public String add(String a, String b) {
System.out.println("Concatenating strings: \"" + a + "\" + \"" + b + "\"");
return a + b;
}
// Constructor overloading (also compile-time polymorphism)
public PolymorphismTypes() {
System.out.println("Default constructor called");
}
public PolymorphismTypes(String message) {
System.out.println("Parameterized constructor: " + message);
}
// === RUNTIME POLYMORPHISM (Method Overriding) ===
static class Vehicle {
public void start() {
System.out.println("Vehicle is starting...");
}
public void stop() {
System.out.println("Vehicle is stopping...");
}
}
static class Car extends Vehicle {
@Override
public void start() {
System.out.println("Car engine is starting with a key...");
}
@Override
public void stop() {
System.out.println("Car is applying brakes to stop...");
}
}
static class Bike extends Vehicle {
@Override
public void start() {
System.out.println("Bike engine is starting with a kick...");
}
// No override for stop() - uses parent's implementation
}
static class ElectricCar extends Car {
@Override
public void start() {
System.out.println("Electric car is starting silently...");
}
// New method specific to ElectricCar
public void chargeBattery() {
System.out.println("Charging electric car battery...");
}
}
public static void main(String[] args) {
System.out.println("=== COMPILE-TIME POLYMORPHISM Examples ===\n");
PolymorphismTypes pt = new PolymorphismTypes();
PolymorphismTypes pt2 = new PolymorphismTypes("Hello World");
System.out.println("\nMethod Overloading Examples:");
System.out.println("Result: " + pt.add(5, 10));
System.out.println("Result: " + pt.add(5.5, 10.5));
System.out.println("Result: " + pt.add(1, 2, 3));
System.out.println("Result: " + pt.add("Hello", " World"));
System.out.println("\n=== RUNTIME POLYMORPHISM Examples ===\n");
// Parent reference to child objects
Vehicle vehicle1 = new Car();
Vehicle vehicle2 = new Bike();
Vehicle vehicle3 = new ElectricCar();
Vehicle vehicle4 = new Vehicle();
Vehicle[] vehicles = {vehicle1, vehicle2, vehicle3, vehicle4};
System.out.println("Starting all vehicles:");
for (Vehicle v : vehicles) {
v.start(); // Dynamic method dispatch - JVM decides which method to call
}
System.out.println("\nStopping all vehicles:");
for (Vehicle v : vehicles) {
v.stop(); // Bike uses Vehicle's stop(), others use their own
}
System.out.println("\n=== Type Checking and Casting ===");
for (Vehicle v : vehicles) {
if (v instanceof ElectricCar) {
ElectricCar ec = (ElectricCar) v;
ec.chargeBattery(); // Call ElectricCar-specific method
}
}
System.out.println("\n=== Key Differences ===");
System.out.println("Compile-Time:");
System.out.println("- Decided by compiler during compilation");
System.out.println("- Based on method signature (name + parameters)");
System.out.println("- Faster, less flexible");
System.out.println("\nRuntime:");
System.out.println("- Decided by JVM during execution");
System.out.println("- Based on actual object type (not reference type)");
System.out.println("- Slower, more flexible, enables extensibility");
}
}
3. Method Overloading (Compile-Time)
Method Overloading allows a class to have multiple methods with the same name but different parameters. The compiler decides which method to call based on the method signature.
public class MethodOverloading {
// === RULES FOR METHOD OVERLOADING ===
// 1. Methods must have same name
// 2. Methods must have different parameters:
// - Different number of parameters
// - Different types of parameters
// - Different order of parameters
// 3. Return type can be same or different
// 4. Access modifier can be same or different
// 5. Can throw different exceptions
// Example 1: Different number of parameters
public void display() {
System.out.println("No parameters");
}
public void display(String message) {
System.out.println("One parameter: " + message);
}
public void display(String message, int times) {
for (int i = 0; i < times; i++) {
System.out.println(message);
}
}
// Example 2: Different types of parameters
public int calculate(int a, int b) {
return a + b;
}
public double calculate(double a, double b) {
return a + b;
}
public String calculate(String a, String b) {
return a + b;
}
// Example 3: Different order of parameters
public void showDetails(int id, String name) {
System.out.println("ID: " + id + ", Name: " + name);
}
public void showDetails(String name, int id) {
System.out.println("Name: " + name + ", ID: " + id);
}
// Example 4: With different return types (VALID)
public int getValue(int x) {
return x * 2;
}
public double getValue(double x) {
return x * 3.14;
}
// Example 5: Variable arguments (varargs)
public int sum(int... numbers) {
int total = 0;
for (int num : numbers) {
total += num;
}
return total;
}
// Example 6: Overloading with type promotion
public void print(int x) {
System.out.println("int: " + x);
}
public void print(long x) {
System.out.println("long: " + x);
}
public void print(double x) {
System.out.println("double: " + x);
}
// === INVALID OVERLOADING Examples ===
// INVALID: Only return type different
// public int invalidMethod() { return 1; }
// public void invalidMethod() { } // COMPILE ERROR
// INVALID: Only access modifier different
// private void anotherMethod() { }
// public void anotherMethod() { } // COMPILE ERROR
// INVALID: Only exception list different
// public void example() { }
// public void example() throws IOException { } // COMPILE ERROR
public static void main(String[] args) {
MethodOverloading mo = new MethodOverloading();
System.out.println("=== Method Overloading Examples ===\n");
System.out.println("1. Different number of parameters:");
mo.display();
mo.display("Hello");
mo.display("Hello", 3);
System.out.println("\n2. Different types of parameters:");
System.out.println("calculate(5, 10) = " + mo.calculate(5, 10));
System.out.println("calculate(5.5, 10.5) = " + mo.calculate(5.5, 10.5));
System.out.println("calculate(\"Hello\", \" World\") = " + mo.calculate("Hello", " World"));
System.out.println("\n3. Different order of parameters:");
mo.showDetails(101, "Alice");
mo.showDetails("Bob", 102);
System.out.println("\n4. With different return types:");
System.out.println("getValue(5) = " + mo.getValue(5));
System.out.println("getValue(5.0) = " + mo.getValue(5.0));
System.out.println("\n5. Variable arguments:");
System.out.println("sum(1, 2, 3) = " + mo.sum(1, 2, 3));
System.out.println("sum(1, 2, 3, 4, 5) = " + mo.sum(1, 2, 3, 4, 5));
System.out.println("sum() = " + mo.sum()); // Zero arguments
System.out.println("\n6. Type promotion in overloading:");
mo.print(10); // Calls print(int)
mo.print(10L); // Calls print(long)
mo.print(10.0); // Calls print(double)
mo.print(10.0f); // float promoted to double
System.out.println("\n=== When Does Overloading Fail? ===");
System.out.println("Ambiguity example:");
// mo.print(10.5f); // Ambiguous: float can go to double or long
System.out.println("\n=== Best Practices for Method Overloading ===");
System.out.println("1. Keep overloaded methods semantically similar");
System.out.println("2. Avoid too many overloaded versions");
System.out.println("3. Use clear, descriptive parameter names");
System.out.println("4. Consider using varargs for variable parameters");
System.out.println("5. Document each overloaded version");
System.out.println("\n=== Common Uses in Java API ===");
System.out.println("- System.out.println(): 10+ overloaded versions");
System.out.println("- String.valueOf(): 9 overloaded versions");
System.out.println("- Arrays.sort(): Multiple versions for different types");
System.out.println("- Collections.sort(): Multiple comparator versions");
}
}
// Real-world example: Bank Account class with constructor overloading
public class ConstructorOverloading {
static class BankAccount {
private String accountNumber;
private String accountHolder;
private double balance;
private String accountType;
private double interestRate;
// === CONSTRUCTOR OVERLOADING ===
// Constructor 1: Minimum required information
public BankAccount(String accountNumber, String accountHolder) {
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
this.balance = 0.0;
this.accountType = "Savings";
this.interestRate = 3.5;
System.out.println("Account created with default settings");
}
// Constructor 2: With initial balance
public BankAccount(String accountNumber, String accountHolder, double initialBalance) {
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
this.balance = initialBalance;
this.accountType = "Savings";
this.interestRate = 3.5;
System.out.println("Account created with initial balance: $" + initialBalance);
}
// Constructor 3: Full specification
public BankAccount(String accountNumber, String accountHolder,
double initialBalance, String accountType, double interestRate) {
this.accountNumber = accountNumber;
this.accountHolder = accountHolder;
this.balance = initialBalance;
this.accountType = accountType;
this.interestRate = interestRate;
System.out.println(accountType + " account created with interest rate: " + interestRate + "%");
}
// Constructor 4: Copy constructor
public BankAccount(BankAccount other) {
this.accountNumber = other.accountNumber + "-COPY";
this.accountHolder = other.accountHolder;
this.balance = other.balance;
this.accountType = other.accountType;
this.interestRate = other.interestRate;
System.out.println("Copy of account created");
}
// === METHOD OVERLOADING in the same class ===
// Deposit methods
public void deposit(double amount) {
balance += amount;
System.out.println("Deposited: $" + amount);
}
public void deposit(double amount, String description) {
balance += amount;
System.out.println("Deposited: $" + amount + " - " + description);
}
public void deposit(double... amounts) {
double total = 0;
for (double amount : amounts) {
total += amount;
}
balance += total;
System.out.println("Deposited multiple amounts totaling: $" + total);
}
// Withdrawal methods
public boolean withdraw(double amount) {
return withdraw(amount, "Standard withdrawal");
}
public boolean withdraw(double amount, String reason) {
if (balance >= amount) {
balance -= amount;
System.out.println("Withdrawn: $" + amount + " for: " + reason);
return true;
} else {
System.out.println("Insufficient funds for: " + reason);
return false;
}
}
// Display methods
public void display() {
display(false);
}
public void display(boolean showDetails) {
System.out.println("\n=== Account Information ===");
System.out.println("Account Holder: " + accountHolder);
System.out.println("Account Number: " + accountNumber);
System.out.println("Balance: $" + balance);
if (showDetails) {
System.out.println("Account Type: " + accountType);
System.out.println("Interest Rate: " + interestRate + "%");
}
}
// Getters
public String getAccountNumber() { return accountNumber; }
public String getAccountHolder() { return accountHolder; }
public double getBalance() { return balance; }
public String getAccountType() { return accountType; }
public double getInterestRate() { return interestRate; }
}
public static void main(String[] args) {
System.out.println("=== Constructor and Method Overloading Example ===\n");
// Using different constructors
BankAccount account1 = new BankAccount("ACC001", "John Doe");
BankAccount account2 = new BankAccount("ACC002", "Jane Smith", 5000.0);
BankAccount account3 = new BankAccount("ACC003", "Bob Johnson",
10000.0, "Checking", 1.5);
BankAccount account4 = new BankAccount(account3); // Copy constructor
System.out.println("\n=== Method Overloading Examples ===\n");
// Deposit methods
account1.deposit(1000.0);
account1.deposit(500.0, "Bonus from bank");
account1.deposit(100.0, 200.0, 300.0);
// Withdrawal methods
account2.withdraw(1000.0);
account2.withdraw(5000.0, "Emergency expense");
account2.withdraw(1000.0); // Should fail
// Display methods
account3.display();
account3.display(true);
System.out.println("\n=== Benefits of Constructor Overloading ===");
System.out.println("1. Flexibility: Different ways to create objects");
System.out.println("2. Convenience: Default values for optional parameters");
System.out.println("3. Readability: Clear intent for different scenarios");
System.out.println("4. Maintainability: Centralized initialization logic");
System.out.println("\n=== Benefits of Method Overloading ===");
System.out.println("1. Intuitive API: Same method name for similar operations");
System.out.println("2. Reduced complexity: Fewer method names to remember");
System.out.println("3. Code clarity: Appropriate method for each use case");
System.out.println("4. Backward compatibility: Add parameters without breaking existing code");
}
}
4. Method Overriding (Runtime)
Method Overriding allows a subclass to provide a specific implementation of a method that is already defined in its superclass. The method in the subclass must have the same signature as the method in the superclass.
import java.util.Date;
public class MethodOverriding {
// === BASE CLASS ===
static class Employee {
private String name;
private int id;
private double salary;
public Employee(String name, int id, double salary) {
this.name = name;
this.id = id;
this.salary = salary;
}
// Method to be overridden by subclasses
public double calculateBonus() {
return salary * 0.10; // Default 10% bonus
}
public void work() {
System.out.println(name + " is working as an employee.");
}
// toString() method - overridden from Object class
@Override
public String toString() {
return "Employee [name=" + name + ", id=" + id + ", salary=$" + salary + "]";
}
// equals() method - overridden from Object class
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Employee employee = (Employee) obj;
return id == employee.id;
}
// hashCode() method - overridden from Object class
@Override
public int hashCode() {
return id;
}
// Getters
public String getName() { return name; }
public int getId() { return id; }
public double getSalary() { return salary; }
}
// === DERIVED CLASSES ===
static class Manager extends Employee {
private int teamSize;
public Manager(String name, int id, double salary, int teamSize) {
super(name, id, salary);
this.teamSize = teamSize;
}
// Override calculateBonus() method
@Override
public double calculateBonus() {
double baseBonus = super.calculateBonus(); // Call parent method
double managerBonus = teamSize * 1000; // Additional bonus
return baseBonus + managerBonus;
}
// Override work() method
@Override
public void work() {
System.out.println(getName() + " is managing a team of " + teamSize + " people.");
}
// Add new method specific to Manager
public void conductMeeting() {
System.out.println(getName() + " is conducting a team meeting.");
}
// Override toString() method
@Override
public String toString() {
return super.toString() + " [Manager, teamSize=" + teamSize + "]";
}
}
static class Developer extends Employee {
private String programmingLanguage;
public Developer(String name, int id, double salary, String programmingLanguage) {
super(name, id, salary);
this.programmingLanguage = programmingLanguage;
}
// Override calculateBonus() method
@Override
public double calculateBonus() {
double baseBonus = super.calculateBonus();
double techBonus = 5000; // Fixed tech bonus
return baseBonus + techBonus;
}
// Override work() method
@Override
public void work() {
System.out.println(getName() + " is coding in " + programmingLanguage + ".");
}
// Add new method specific to Developer
public void debugCode() {
System.out.println(getName() + " is debugging " + programmingLanguage + " code.");
}
// Override toString() method
@Override
public String toString() {
return super.toString() + " [Developer, language=" + programmingLanguage + "]";
}
}
static class Intern extends Employee {
private Date internshipEndDate;
public Intern(String name, int id, double salary, Date internshipEndDate) {
super(name, id, salary);
this.internshipEndDate = internshipEndDate;
}
// Override calculateBonus() method - Interns get no bonus
@Override
public double calculateBonus() {
return 0.0;
}
// Override work() method
@Override
public void work() {
System.out.println(getName() + " is learning and assisting team members.");
}
// Override toString() method
@Override
public String toString() {
return super.toString() + " [Intern, endDate=" + internshipEndDate + "]";
}
}
public static void main(String[] args) {
System.out.println("=== Method Overriding Demonstration ===\n");
// Create different types of employees
Employee emp1 = new Employee("John Doe", 101, 50000);
Employee emp2 = new Manager("Alice Smith", 102, 80000, 5);
Employee emp3 = new Developer("Bob Johnson", 103, 60000, "Java");
Employee emp4 = new Intern("Charlie Brown", 104, 20000, new Date());
Employee[] employees = {emp1, emp2, emp3, emp4};
System.out.println("=== Employee Information ===");
for (Employee emp : employees) {
System.out.println(emp); // Calls overridden toString() method
}
System.out.println("\n=== Bonus Calculation (Polymorphic Behavior) ===");
for (Employee emp : employees) {
double bonus = emp.calculateBonus(); // Dynamic method dispatch
System.out.println(emp.getName() + "'s bonus: $" + bonus);
}
System.out.println("\n=== Work Activities (Polymorphic Behavior) ===");
for (Employee emp : employees) {
emp.work(); // Each employee works differently
}
System.out.println("\n=== Type Checking and Specific Methods ===");
for (Employee emp : employees) {
if (emp instanceof Manager) {
Manager manager = (Manager) emp;
manager.conductMeeting();
} else if (emp instanceof Developer) {
Developer developer = (Developer) emp;
developer.debugCode();
}
}
System.out.println("\n=== Rules for Method Overriding ===");
System.out.println("1. Same method signature (name + parameters)");
System.out.println("2. Return type should be same or covariant (subtype)");
System.out.println("3. Access modifier cannot be more restrictive");
System.out.println("4. Cannot override final, static, or private methods");
System.out.println("5. Can throw same, subclass, or no exception (not broader)");
System.out.println("\n=== @Override Annotation ===");
System.out.println("Benefits of using @Override:");
System.out.println("1. Compile-time checking: Ensures method is actually overriding");
System.out.println("2. Code clarity: Clearly indicates overriding intent");
System.out.println("3. Prevents errors: Catches typos in method names");
System.out.println("\n=== Common Overridden Methods from Object Class ===");
System.out.println("1. toString(): Returns string representation");
System.out.println("2. equals(): Compares object equality");
System.out.println("3. hashCode(): Returns hash code value");
System.out.println("4. clone(): Creates and returns a copy");
System.out.println("5. finalize(): Called before garbage collection");
System.out.println("\n=== Best Practices ===");
System.out.println("1. Always use @Override annotation");
System.out.println("2. Call super.method() when extending behavior");
System.out.println("3. Maintain Liskov Substitution Principle");
System.out.println("4. Document behavior changes in overridden methods");
System.out.println("5. Test polymorphic behavior thoroughly");
}
}
5. Abstract Classes & Interfaces
Abstract classes and interfaces are key tools for implementing polymorphism in Java. They define contracts that multiple classes can implement in different ways.
Abstract Classes
- Partial Implementation: Can have both abstract and concrete methods
- Constructor: Can have constructors (but cannot be instantiated)
- State: Can have instance variables
- Access Modifiers: Methods can be public, protected, private
- Single Inheritance: A class can extend only one abstract class
- Use When: Sharing code among closely related classes
Interfaces
- Pure Contract: Only method declarations (before Java 8)
- No Constructor: Cannot have constructors
- No State: Only constants (static final variables)
- Public Methods: All methods are implicitly public
- Multiple Inheritance: A class can implement multiple interfaces
- Use When: Defining capabilities across unrelated classes
// === INTERFACE EXAMPLE ===
interface PaymentMethod {
// Constants (implicitly public static final)
double MAX_AMOUNT = 1000000.0;
// Abstract methods (before Java 8)
boolean processPayment(double amount);
String getPaymentDetails();
// Default method (Java 8+)
default boolean validateAmount(double amount) {
return amount > 0 && amount <= MAX_AMOUNT;
}
// Static method (Java 8+)
static String getPaymentSystem() {
return "Global Payment System";
}
// Private method (Java 9+)
private void logTransaction(String details) {
System.out.println("Transaction logged: " + details);
}
}
// === ABSTRACT CLASS EXAMPLE ===
abstract class Shape {
// Instance variables
protected String color;
protected boolean filled;
// Constructor
public Shape(String color, boolean filled) {
this.color = color;
this.filled = filled;
}
// Abstract methods (must be implemented by subclasses)
public abstract double calculateArea();
public abstract double calculatePerimeter();
// Concrete methods
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public boolean isFilled() {
return filled;
}
public void setFilled(boolean filled) {
this.filled = filled;
}
// Can have final methods
public final String getShapeType() {
return this.getClass().getSimpleName();
}
}
// === CONCRETE CLASSES IMPLEMENTING INTERFACE ===
class CreditCardPayment implements PaymentMethod {
private String cardNumber;
private String cardHolder;
private String expiryDate;
public CreditCardPayment(String cardNumber, String cardHolder, String expiryDate) {
this.cardNumber = cardNumber;
this.cardHolder = cardHolder;
this.expiryDate = expiryDate;
}
@Override
public boolean processPayment(double amount) {
if (!validateAmount(amount)) {
System.out.println("Invalid amount: $" + amount);
return false;
}
System.out.println("Processing credit card payment of $" + amount);
System.out.println("Card: " + cardNumber.substring(cardNumber.length() - 4));
return true;
}
@Override
public String getPaymentDetails() {
return "Credit Card - Holder: " + cardHolder + ", Expires: " + expiryDate;
}
}
class PayPalPayment implements PaymentMethod {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
@Override
public boolean processPayment(double amount) {
if (!validateAmount(amount)) {
System.out.println("Invalid amount: $" + amount);
return false;
}
System.out.println("Processing PayPal payment of $" + amount);
System.out.println("Email: " + email);
return true;
}
@Override
public String getPaymentDetails() {
return "PayPal - Email: " + email;
}
}
// === CONCRETE CLASSES EXTENDING ABSTRACT CLASS ===
class Circle extends Shape {
private double radius;
public Circle(String color, boolean filled, double radius) {
super(color, filled);
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
@Override
public double calculatePerimeter() {
return 2 * Math.PI * radius;
}
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = radius;
}
@Override
public String toString() {
return "Circle [radius=" + radius + ", color=" + color + ", filled=" + filled + "]";
}
}
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(String color, boolean filled, double width, double height) {
super(color, filled);
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
@Override
public double calculatePerimeter() {
return 2 * (width + height);
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
@Override
public String toString() {
return "Rectangle [width=" + width + ", height=" + height +
", color=" + color + ", filled=" + filled + "]";
}
}
// === CLASS USING BOTH ABSTRACT CLASS AND INTERFACE ===
abstract class BankAccount implements PaymentMethod {
protected String accountNumber;
protected double balance;
public BankAccount(String accountNumber, double initialBalance) {
this.accountNumber = accountNumber;
this.balance = initialBalance;
}
// Abstract method specific to BankAccount
public abstract double calculateInterest();
// Common implementation for PaymentMethod
@Override
public boolean processPayment(double amount) {
if (amount <= balance) {
balance -= amount;
System.out.println("Bank transfer of $" + amount + " processed");
return true;
}
return false;
}
}
public class AbstractVsInterface {
public static void main(String[] args) {
System.out.println("=== Abstract Classes vs Interfaces ===\n");
System.out.println("=== Payment Methods (Interface Example) ===");
PaymentMethod[] payments = {
new CreditCardPayment("1234-5678-9012-3456", "John Doe", "12/25"),
new PayPalPayment("john.doe@email.com")
};
for (PaymentMethod payment : payments) {
System.out.println("\n" + payment.getPaymentDetails());
System.out.println("Payment System: " + PaymentMethod.getPaymentSystem());
payment.processPayment(150.75);
}
System.out.println("\n=== Shapes (Abstract Class Example) ===");
Shape[] shapes = {
new Circle("Red", true, 5.0),
new Rectangle("Blue", false, 4.0, 6.0)
};
for (Shape shape : shapes) {
System.out.println("\n" + shape);
System.out.println("Type: " + shape.getShapeType());
System.out.println("Area: " + shape.calculateArea());
System.out.println("Perimeter: " + shape.calculatePerimeter());
}
System.out.println("\n=== Key Differences ===");
System.out.println("Abstract Classes:");
System.out.println("- Can have constructors");
System.out.println("- Can have instance variables");
System.out.println("- Can have private/protected methods");
System.out.println("- Single inheritance");
System.out.println("- Use 'extends' keyword");
System.out.println("\nInterfaces:");
System.out.println("- Cannot have constructors");
System.out.println("- Only constants (no instance variables)");
System.out.println("- Methods are implicitly public");
System.out.println("- Multiple inheritance");
System.out.println("- Use 'implements' keyword");
System.out.println("\n=== When to Use What? ===");
System.out.println("Use Abstract Class when:");
System.out.println("- Sharing code among closely related classes");
System.out.println("- Expecting classes to have common state/behavior");
System.out.println("- Need to declare non-public methods");
System.out.println("- Plan to add methods in future (can be concrete)");
System.out.println("\nUse Interface when:");
System.out.println("- Unrelated classes need to share capability");
System.out.println("- Multiple inheritance is needed");
System.out.println("- Defining a contract/API");
System.out.println("- Future expansion with default methods");
System.out.println("\n=== Java 8+ Enhancements ===");
System.out.println("1. Default methods: Add methods without breaking implementations");
System.out.println("2. Static methods: Utility methods in interfaces");
System.out.println("3. Functional interfaces: Single abstract method (for lambdas)");
System.out.println("4. Private methods: Helper methods in interfaces (Java 9+)");
}
}
6. Real-World Polymorphism Examples
E-commerce System
// Real-world e-commerce system demonstrating polymorphism
import java.util.ArrayList;
import java.util.List;
public class ECommerceSystem {
// === INTERFACES ===
interface Discount {
double applyDiscount(double originalPrice);
String getDescription();
}
interface TaxCalculator {
double calculateTax(double price);
}
interface PaymentProcessor {
boolean processPayment(double amount);
String getProcessorName();
}
interface ShippingMethod {
double calculateShippingCost(double weight, String destination);
int getEstimatedDays();
}
// === DISCOUNT IMPLEMENTATIONS ===
static class PercentageDiscount implements Discount {
private double percentage;
private String description;
public PercentageDiscount(double percentage, String description) {
this.percentage = percentage;
this.description = description;
}
@Override
public double applyDiscount(double originalPrice) {
return originalPrice * (1 - percentage / 100);
}
@Override
public String getDescription() {
return description + " (" + percentage + "%)";
}
}
static class FixedAmountDiscount implements Discount {
private double amount;
private String description;
public FixedAmountDiscount(double amount, String description) {
this.amount = amount;
this.description = description;
}
@Override
public double applyDiscount(double originalPrice) {
return Math.max(0, originalPrice - amount);
}
@Override
public String getDescription() {
return description + " ($" + amount + ")";
}
}
static class BuyOneGetOneDiscount implements Discount {
@Override
public double applyDiscount(double originalPrice) {
return originalPrice / 2; // Pay for one, get one free
}
@Override
public String getDescription() {
return "Buy One Get One Free";
}
}
// === TAX CALCULATOR IMPLEMENTATIONS ===
static class USATaxCalculator implements TaxCalculator {
private String state;
public USATaxCalculator(String state) {
this.state = state;
}
@Override
public double calculateTax(double price) {
double baseRate = 0.08; // 8% base tax
double stateRate = getStateTaxRate(state);
return price * (baseRate + stateRate);
}
private double getStateTaxRate(String state) {
switch (state.toUpperCase()) {
case "CA": return 0.025;
case "NY": return 0.045;
case "TX": return 0.00;
default: return 0.02;
}
}
}
static class EuropeTaxCalculator implements TaxCalculator {
private String country;
public EuropeTaxCalculator(String country) {
this.country = country;
}
@Override
public double calculateTax(double price) {
// VAT rates in Europe
switch (country.toUpperCase()) {
case "GERMANY": return price * 0.19; // 19% VAT
case "FRANCE": return price * 0.20; // 20% VAT
case "UK": return price * 0.20; // 20% VAT
default: return price * 0.21; // 21% default VAT
}
}
}
static class TaxFreeCalculator implements TaxCalculator {
@Override
public double calculateTax(double price) {
return 0.0; // No tax
}
}
// === PAYMENT PROCESSOR IMPLEMENTATIONS ===
static class StripeProcessor implements PaymentProcessor {
@Override
public boolean processPayment(double amount) {
System.out.println("Processing $" + amount + " via Stripe");
// Simulate payment processing
return Math.random() > 0.1; // 90% success rate
}
@Override
public String getProcessorName() {
return "Stripe";
}
}
static class PayPalProcessor implements PaymentProcessor {
@Override
public boolean processPayment(double amount) {
System.out.println("Processing $" + amount + " via PayPal");
// Simulate payment processing
return Math.random() > 0.05; // 95% success rate
}
@Override
public String getProcessorName() {
return "PayPal";
}
}
static class BankTransferProcessor implements PaymentProcessor {
@Override
public boolean processPayment(double amount) {
System.out.println("Processing $" + amount + " via Bank Transfer");
// Bank transfers are always successful (once initiated)
return true;
}
@Override
public String getProcessorName() {
return "Bank Transfer";
}
}
// === SHIPPING METHOD IMPLEMENTATIONS ===
static class StandardShipping implements ShippingMethod {
@Override
public double calculateShippingCost(double weight, String destination) {
double baseCost = 5.99;
double weightCost = weight * 0.5;
return baseCost + weightCost;
}
@Override
public int getEstimatedDays() {
return 5;
}
}
static class ExpressShipping implements ShippingMethod {
@Override
public double calculateShippingCost(double weight, String destination) {
double baseCost = 15.99;
double weightCost = weight * 1.0;
return baseCost + weightCost;
}
@Override
public int getEstimatedDays() {
return 2;
}
}
static class InternationalShipping implements ShippingMethod {
@Override
public double calculateShippingCost(double weight, String destination) {
double baseCost = 25.99;
double weightCost = weight * 2.0;
return baseCost + weightCost;
}
@Override
public int getEstimatedDays() {
return 10;
}
}
// === PRODUCT CLASS ===
static class Product {
private String name;
private double price;
private double weight;
public Product(String name, double price, double weight) {
this.name = name;
this.price = price;
this.weight = weight;
}
public String getName() { return name; }
public double getPrice() { return price; }
public double getWeight() { return weight; }
}
// === ORDER CLASS ===
static class Order {
private List products = new ArrayList<>();
private Discount discount;
private TaxCalculator taxCalculator;
private PaymentProcessor paymentProcessor;
private ShippingMethod shippingMethod;
public void addProduct(Product product) {
products.add(product);
}
public void setDiscount(Discount discount) {
this.discount = discount;
}
public void setTaxCalculator(TaxCalculator taxCalculator) {
this.taxCalculator = taxCalculator;
}
public void setPaymentProcessor(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public void setShippingMethod(ShippingMethod shippingMethod) {
this.shippingMethod = shippingMethod;
}
public double calculateTotal() {
double subtotal = products.stream()
.mapToDouble(Product::getPrice)
.sum();
// Apply discount if available
if (discount != null) {
subtotal = discount.applyDiscount(subtotal);
System.out.println("Applied discount: " + discount.getDescription());
}
// Add tax
double tax = taxCalculator.calculateTax(subtotal);
System.out.println("Tax: $" + String.format("%.2f", tax));
// Add shipping
double totalWeight = products.stream()
.mapToDouble(Product::getWeight)
.sum();
double shipping = shippingMethod.calculateShippingCost(totalWeight, "USA");
System.out.println("Shipping: $" + String.format("%.2f", shipping));
System.out.println("Estimated delivery: " + shippingMethod.getEstimatedDays() + " days");
return subtotal + tax + shipping;
}
public boolean processOrder() {
double total = calculateTotal();
System.out.println("\nTotal amount: $" + String.format("%.2f", total));
System.out.println("Processing payment with " + paymentProcessor.getProcessorName());
boolean paymentSuccess = paymentProcessor.processPayment(total);
if (paymentSuccess) {
System.out.println("Payment successful! Order confirmed.");
return true;
} else {
System.out.println("Payment failed. Please try again.");
return false;
}
}
}
public static void main(String[] args) {
System.out.println("=== E-Commerce System with Polymorphism ===\n");
// Create products
Product laptop = new Product("Laptop", 999.99, 5.0);
Product mouse = new Product("Wireless Mouse", 29.99, 0.5);
Product keyboard = new Product("Mechanical Keyboard", 89.99, 2.0);
// Create order
Order order = new Order();
order.addProduct(laptop);
order.addProduct(mouse);
order.addProduct(keyboard);
// Set up polymorphic components
order.setDiscount(new PercentageDiscount(10.0, "Summer Sale"));
order.setTaxCalculator(new USATaxCalculator("CA"));
order.setPaymentProcessor(new StripeProcessor());
order.setShippingMethod(new ExpressShipping());
System.out.println("=== Order Summary ===");
System.out.println("Products:");
System.out.println("- " + laptop.getName() + ": $" + laptop.getPrice());
System.out.println("- " + mouse.getName() + ": $" + mouse.getPrice());
System.out.println("- " + keyboard.getName() + ": $" + keyboard.getPrice());
System.out.println("\n=== Calculation ===");
boolean success = order.processOrder();
System.out.println("\n=== Trying Different Combinations ===");
// Different discount
System.out.println("\n--- With Fixed Amount Discount ---");
Order order2 = new Order();
order2.addProduct(laptop);
order2.setDiscount(new FixedAmountDiscount(50.0, "New Customer Discount"));
order2.setTaxCalculator(new TaxFreeCalculator());
order2.setPaymentProcessor(new PayPalProcessor());
order2.setShippingMethod(new StandardShipping());
order2.processOrder();
// International order
System.out.println("\n--- International Order ---");
Order order3 = new Order();
order3.addProduct(laptop);
order3.setDiscount(new BuyOneGetOneDiscount());
order3.setTaxCalculator(new EuropeTaxCalculator("GERMANY"));
order3.setPaymentProcessor(new BankTransferProcessor());
order3.setShippingMethod(new InternationalShipping());
order3.processOrder();
System.out.println("\n=== Benefits of Polymorphism in this System ===");
System.out.println("1. Flexibility: Easy to add new discount/tax/shipping types");
System.out.println("2. Maintainability: Changes in one component don't affect others");
System.out.println("3. Testability: Easy to mock components for testing");
System.out.println("4. Scalability: New payment processors can be added easily");
System.out.println("5. Code Reuse: Common interfaces reduce duplication");
}
}
7. Best Practices & Common Pitfalls
- Violating Liskov Substitution Principle: Subclass changes behavior unexpectedly
- Overusing instanceof: Too many type checks indicate design issues
- Ignoring @Override annotation: Missing compiler checks for overrides
- Method hiding instead of overriding: Using static methods incorrectly
- Deep inheritance hierarchies: Complex, hard-to-maintain code
- Covariant return type errors: Incorrect return types in overrides
Polymorphism Best Practices
- Follow Liskov Substitution Principle
- Use interfaces for flexibility
- Prefer composition over deep inheritance
- Always use @Override annotation
- Design for extension, not modification
- Keep inheritance hierarchies shallow
- Use abstract classes for code reuse
Performance Considerations
- Method calls have minimal overhead
- Virtual method tables (vtables) enable fast dispatch
- JIT compiler optimizes frequent virtual calls
- Use final methods/classes when possible for optimization
- Consider costs of excessive abstraction
- Profile before optimizing polymorphic calls
Liskov Substitution Principle (LSP)
Definition: Objects of a superclass should be replaceable with objects of a subclass without affecting correctness.
Example: If Bird is a subclass of Animal, you should be able to use Bird wherever Animal is expected.
Violation: Penguin extends Bird but cannot fly() - breaks LSP if Bird has fly() method.
public class PolymorphismChecklist {
public static void polymorphismChecklist() {
System.out.println("=== Polymorphism Implementation Checklist ===\n");
System.out.println("1. ✅ Understand the Problem");
System.out.println(" - Is polymorphism the right solution?");
System.out.println(" - Identify common behaviors/attributes");
System.out.println(" - Plan inheritance hierarchy or interfaces");
System.out.println("\n2. ✅ Design the Interface/Abstract Class");
System.out.println(" - Define common method signatures");
System.out.println(" - Consider access modifiers");
System.out.println(" - Plan for future extensions");
System.out.println("\n3. ✅ Implement Method Overriding");
System.out.println(" - Use @Override annotation");
System.out.println(" - Maintain method contracts");
System.out.println(" - Call super.method() when appropriate");
System.out.println("\n4. ✅ Implement Method Overloading");
System.out.println(" - Ensure method signatures differ");
System.out.println(" - Keep overloaded methods semantically similar");
System.out.println(" - Consider using varargs for flexibility");
System.out.println("\n5. ✅ Test Polymorphic Behavior");
System.out.println(" - Test with different concrete types");
System.out.println(" - Verify Liskov Substitution Principle");
System.out.println(" - Test edge cases and null values");
System.out.println("\n6. ✅ Consider Performance");
System.out.println(" - Virtual method call overhead is minimal");
System.out.println(" - Use final for methods that won't be overridden");
System.out.println(" - Profile before optimizing");
System.out.println("\n7. ✅ Document the Design");
System.out.println(" - Document intended polymorphic behavior");
System.out.println(" - Note any assumptions or constraints");
System.out.println(" - Provide examples of usage");
}
// Example: Well-designed polymorphic hierarchy
interface Notification {
void send(String message);
String getType();
}
static class EmailNotification implements Notification {
private String emailAddress;
public EmailNotification(String emailAddress) {
this.emailAddress = emailAddress;
}
@Override
public void send(String message) {
System.out.println("Sending email to " + emailAddress + ": " + message);
}
@Override
public String getType() {
return "Email";
}
}
static class SMSNotification implements Notification {
private String phoneNumber;
public SMSNotification(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
@Override
public void send(String message) {
System.out.println("Sending SMS to " + phoneNumber + ": " + message);
}
@Override
public String getType() {
return "SMS";
}
}
static class PushNotification implements Notification {
private String deviceId;
public PushNotification(String deviceId) {
this.deviceId = deviceId;
}
@Override
public void send(String message) {
System.out.println("Sending push notification to device " + deviceId + ": " + message);
}
@Override
public String getType() {
return "Push";
}
}
static class NotificationService {
private List notifications = new ArrayList<>();
public void addNotification(Notification notification) {
notifications.add(notification);
}
public void broadcast(String message) {
System.out.println("Broadcasting message: " + message);
for (Notification notification : notifications) {
notification.send(message);
}
}
public void sendToType(String message, String type) {
for (Notification notification : notifications) {
if (notification.getType().equals(type)) {
notification.send(message);
}
}
}
}
public static void main(String[] args) {
polymorphismChecklist();
System.out.println("\n=== Example: Notification System ===");
NotificationService service = new NotificationService();
service.addNotification(new EmailNotification("user@example.com"));
service.addNotification(new SMSNotification("+1234567890"));
service.addNotification(new PushNotification("device-123"));
System.out.println("\n=== Broadcast to All ===");
service.broadcast("System maintenance scheduled for tonight.");
System.out.println("\n=== Send to Specific Type ===");
service.sendToType("Your order has shipped!", "SMS");
System.out.println("\n=== Benefits of This Design ===");
System.out.println("1. Easy to add new notification types");
System.out.println("2. Single service handles all notification types");
System.out.println("3. Type-specific behavior encapsulated");
System.out.println("4. Easy to test each notification type");
System.out.println("5. Flexible routing (broadcast or type-specific)");
System.out.println("\n=== Final Recommendations ===");
System.out.println("For new projects:");
System.out.println("- Start with interfaces for maximum flexibility");
System.out.println("- Use abstract classes only when sharing code");
System.out.println("- Design small, focused interfaces (Interface Segregation)");
System.out.println("\nFor existing code:");
System.out.println("- Refactor to use polymorphism where appropriate");
System.out.println("- Replace conditional logic with polymorphism");
System.out.println("- Consider strategy pattern for varying algorithms");
}
}