Your payment system accepts CreditCard, PayPal, and BankTransfer. Instead of separate code paths for each, polymorphism lets you treat them all as Payment objects - calling process() works correctly for each type.

Runtime polymorphism

Parent reference can hold child object.

Runtime.java
// Runtime Polymorphism

public class Runtime {
    public static void main(String[] args) {
        System.out.println("=== Runtime Polymorphism ===\n");

        // Same type reference, different objects
        Animal a1 = new Dog();
        Animal a2 = new Cat();
        Animal a3 = new Bird();

        // Each calls its OWN version of makeSound()
        System.out.println("Calling makeSound() on each:");
        a1.makeSound();  // Woof!
        a2.makeSound();  // Meow!
        a3.makeSound();  // Tweet!

        System.out.println("\n=== Why 'Polymorphism'? ===");
        System.out.println("""
        Poly = Many
        Morph = Forms

        Same method name (makeSound)
        Many different behaviors (woof, meow, tweet)
        """);

        System.out.println("=== Method Called at Runtime ===");

        // The actual method called depends on the OBJECT
        int choice = ;
        Animal mystery = getAnimal(choice);
        System.out.println("Mystery animal says:");
        mystery.makeSound();  // Which version? Depends on actual object!
    }

    static Animal getAnimal(int choice) {
        return switch(choice) {
            case 0 -> new Dog();
            case 1 -> new Cat();
            default -> new Bird();
        };
    }
}

class Animal {
    void makeSound() {
        System.out.println("Some generic sound");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Woof! Woof!");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("Meow!");
    }
}

class Bird extends Animal {
    @Override
    void makeSound() {
        System.out.println("Tweet! Tweet!");
    }
}

// Runtime Polymorphism

public class Runtime {
    public static void main(String[] args) {
        System.out.println("=== Runtime Polymorphism ===\n");

        // Same type reference, different objects
        Animal a1 = new Dog();
        Animal a2 = new Cat();
        Animal a3 = new Bird();

        // Each calls its OWN version of makeSound()
        System.out.println("Calling makeSound() on each:");
        a1.makeSound();  // Woof!
        a2.makeSound();  // Meow!
        a3.makeSound();  // Tweet!

        System.out.println("\n=== Why 'Polymorphism'? ===");
        System.out.println("""
        Poly = Many
        Morph = Forms

        Same method name (makeSound)
        Many different behaviors (woof, meow, tweet)
        """);

        System.out.println("=== Method Called at Runtime ===");

        // The actual method called depends on the OBJECT
        int choice = ;
        Animal mystery = getAnimal(choice);
        System.out.println("Mystery animal says:");
        mystery.makeSound();  // Which version? Depends on actual object!
    }

    static Animal getAnimal(int choice) {
        return switch(choice) {
            case 0 -> new Dog();
            case 1 -> new Cat();
            default -> new Bird();
        };
    }
}

class Animal {
    void makeSound() {
        System.out.println("Some generic sound");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Woof! Woof!");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("Meow!");
    }
}

class Bird extends Animal {
    @Override
    void makeSound() {
        System.out.println("Tweet! Tweet!");
    }
}

// Runtime Polymorphism

public class Runtime {
    public static void main(String[] args) {
        System.out.println("=== Runtime Polymorphism ===\n");

        // Same type reference, different objects
        Animal a1 = new Dog();
        Animal a2 = new Cat();
        Animal a3 = new Bird();

        // Each calls its OWN version of makeSound()
        System.out.println("Calling makeSound() on each:");
        a1.makeSound();  // Woof!
        a2.makeSound();  // Meow!
        a3.makeSound();  // Tweet!

        System.out.println("\n=== Why 'Polymorphism'? ===");
        System.out.println("""
        Poly = Many
        Morph = Forms

        Same method name (makeSound)
        Many different behaviors (woof, meow, tweet)
        """);

        System.out.println("=== Method Called at Runtime ===");

        // The actual method called depends on the OBJECT
        int choice = ;
        Animal mystery = getAnimal(choice);
        System.out.println("Mystery animal says:");
        mystery.makeSound();  // Which version? Depends on actual object!
    }

    static Animal getAnimal(int choice) {
        return switch(choice) {
            case 0 -> new Dog();
            case 1 -> new Cat();
            default -> new Bird();
        };
    }
}

class Animal {
    void makeSound() {
        System.out.println("Some generic sound");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("Woof! Woof!");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("Meow!");
    }
}

class Bird extends Animal {
    @Override
    void makeSound() {
        System.out.println("Tweet! Tweet!");
    }
}

Animal a = new Dog() - variable type is Animal, object is Dog.

polymorphism Same code works with different types. Parent reference, child behavior.

Parent reference to child

Store different subtypes in parent-type variable.

ParentReference.java
// Using Parent Type to Reference Child Objects

public class ParentReference {
    public static void main(String[] args) {
        System.out.println("=== Parent Reference, Child Object ===\n");

        // Different declaration styles
        Dog dog = new Dog("Buddy");
        Animal animal = new Dog("Max");

        // Both work for inherited methods
        System.out.println("Both can make sounds:");
        dog.makeSound();
        animal.makeSound();

        System.out.println("\n=== Access Differences ===");

        // Dog reference: full access
        dog.makeSound();   // OK - inherited
        dog.fetch();       // OK - Dog-specific

        // Animal reference: limited access
        animal.makeSound();  // OK - defined in Animal
        // animal.fetch();   // ERROR! Animal doesn't know about fetch

        System.out.println("\n=== Why Use Parent Reference? ===");

        // Flexibility: accept any animal
        feedAnimal(new Dog("Rex"));
        feedAnimal(new Cat("Whiskers"));

        System.out.println("\n=== Upcasting (Automatic) ===");

        Dog rex = new Dog("Rex");
        Animal asAnimal = rex;
        System.out.println("Upcasted: Dog → Animal");

        System.out.println("\n=== Downcasting (Manual) ===");

        Animal mystery = new Dog("Scout");
        Dog backToDog = (Dog) mystery;
        backToDog.fetch();  // Now we can call fetch!
        System.out.println("Downcasted: Animal → Dog");
    }

    // Method accepts ANY Animal
    static void feedAnimal(Animal a) {
        System.out.println("Feeding the animal...");
        a.makeSound();  // Works for any animal type
    }
}

class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    void makeSound() {
        System.out.println(name + " makes a sound");
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + " barks: Woof!");
    }

    void fetch() {
        System.out.println(name + " fetches the ball!");
    }
}

class Cat extends Animal {
    Cat(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + " meows: Meow!");
    }
}

Method called depends on actual object type, not variable type.

Compile-time polymorphism

Method overloading - same name, different parameters.

MethodOverloading.java
// Compile-Time Polymorphism (Method Overloading)

public class MethodOverloading {
    public static void main(String[] args) {
        System.out.println("=== Method Overloading ===\n");

        Calculator calc = new Calculator();

        // Same method name, different parameters
        System.out.println("add(5, 3) = " + calc.add(5, 3));
        System.out.println("add(5, 3, 2) = " + calc.add(5, 3, 2));
        System.out.println("add(5.5, 3.3) = " + calc.add(5.5, 3.3));
        System.out.println("add(\"Hello\", \"World\") = " + calc.add("Hello", "World"));

        System.out.println("\n=== Why 'Compile-Time'? ===");
        System.out.println("""
        Compiler decides which method to call
        based on arguments at compile time.

        add(5, 3)       → calls add(int, int)
        add(5, 3, 2)    → calls add(int, int, int)
        add(5.5, 3.3)   → calls add(double, double)
        add("A", "B")   → calls add(String, String)
        """);

        System.out.println("=== Overloading Rules ===");

        Printer printer = new Printer();

        // Different number of parameters
        printer.print("Hello");
        printer.print("Hello", 3);

        // Different parameter types
        printer.print(42);
        printer.print(3.14);

        // Different parameter order
        printer.display("Name", 1);
        printer.display(1, "Name");

        System.out.println("\n=== Return Type Doesn't Count ===");
        System.out.println("""
        // INVALID overloading:
        int getValue() { return 1; }
        double getValue() { return 1.0; }  // ERROR!

        Return type alone cannot distinguish methods.
        Parameters must be different.
        """);
    }
}

class Calculator {
    // Overloaded add methods

    int add(int a, int b) {
        return a + b;
    }

    int add(int a, int b, int c) {
        return a + b + c;
    }

    double add(double a, double b) {
        return a + b;
    }

    String add(String a, String b) {
        return a + " " + b;
    }
}

class Printer {
    void print(String message) {
        System.out.println("String: " + message);
    }

    void print(String message, int times) {
        for (int i = 0; i < times; i++) {
            System.out.println(message);
        }
    }

    void print(int number) {
        System.out.println("Integer: " + number);
    }

    void print(double number) {
        System.out.println("Double: " + number);
    }

    void display(String label, int value) {
        System.out.println(label + " = " + value);
    }

    void display(int value, String label) {
        System.out.println(value + ": " + label);
    }
}

Compiler chooses method based on argument types at compile time.

overloading Same method name, different parameters. Resolved at compile time.

Arrays of polymorphic objects

Store different subtypes in one array.

ArrayPolymorphism.java
// Arrays and Collections with Polymorphism

public class ArrayPolymorphism {
    public static void main(String[] args) {
        System.out.println("=== Polymorphic Array ===\n");

        // Array of parent type holding different child objects
        Shape[] shapes = new Shape[4];
        shapes[0] = new Circle(5.0);
        shapes[1] = new Rectangle(4.0, 6.0);
        shapes[2] = new Triangle(3.0, 4.0);
        shapes[3] = new Circle(2.5);

        // Process all shapes uniformly
        System.out.println("Drawing all shapes:");
        for (Shape shape : shapes) {
            shape.draw();
        }

        System.out.println("\n=== Calculating Total Area ===");

        double totalArea = 0;
        for (Shape shape : shapes) {
            double area = shape.getArea();
            System.out.println("Area: " + String.format("%.2f", area));
            totalArea += area;
        }
        System.out.println("Total: " + String.format("%.2f", totalArea));

        System.out.println("\n=== Array Initialization Shorthand ===");

        Shape[] moreShapes = {
            new Circle(1.0),
            new Rectangle(2.0, 3.0),
            new Triangle(4.0, 5.0)
        };

        for (Shape s : moreShapes) {
            s.draw();
        }

        System.out.println("\n=== Pass Array to Method ===");

        printShapeInfo(shapes);
    }

    // Method accepts array of Shape
    static void printShapeInfo(Shape[] shapes) {
        System.out.println("Processing " + shapes.length + " shapes:");
        for (int i = 0; i < shapes.length; i++) {
            System.out.println((i + 1) + ". " + shapes[i].getName() +
                             " - Area: " + String.format("%.2f", shapes[i].getArea()));
        }
    }
}

class Shape {
    String getName() {
        return "Shape";
    }

    void draw() {
        System.out.println("Drawing a shape");
    }

    double getArea() {
        return 0;
    }
}

class Circle extends Shape {
    double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    String getName() {
        return "Circle (r=" + radius + ")";
    }

    @Override
    void draw() {
        System.out.println("Drawing circle with radius " + radius);
    }

    @Override
    double getArea() {
        return Math.PI * radius * radius;
    }
}

class Rectangle extends Shape {
    double width, height;

    Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    String getName() {
        return "Rectangle (" + width + "x" + height + ")";
    }

    @Override
    void draw() {
        System.out.println("Drawing rectangle " + width + " x " + height);
    }

    @Override
    double getArea() {
        return width * height;
    }
}

class Triangle extends Shape {
    double base, height;

    Triangle(double base, double height) {
        this.base = base;
        this.height = height;
    }

    @Override
    String getName() {
        return "Triangle (b=" + base + ", h=" + height + ")";
    }

    @Override
    void draw() {
        System.out.println("Drawing triangle with base " + base + " and height " + height);
    }

    @Override
    double getArea() {
        return 0.5 * base * height;
    }
}

Animal[] animals can hold Dog, Cat, Bird objects. Loop calls each's method.

Type checking with instanceof

Check actual type at runtime.

Instanceof.java
// Type Checking with instanceof

public class Instanceof {
    public static void main(String[] args) {
        System.out.println("=== instanceof Operator ===\n");

        // Create various animals
        Animal[] animals = {
            new Dog("Buddy"),
            new Cat("Whiskers"),
            new Dog("Max"),
            new Bird("Tweety")
        };

        // Check types
        for (Animal animal : animals) {
            System.out.print(animal.name + ": ");

            if (animal instanceof Dog) {
                System.out.println("is a Dog");
            } else if (animal instanceof Cat) {
                System.out.println("is a Cat");
            } else if (animal instanceof Bird) {
                System.out.println("is a Bird");
            }
        }

        System.out.println("\n=== Safe Downcasting ===");

        Animal mystery = ;

        // UNSAFE: Could throw ClassCastException
        // Cat cat = (Cat) mystery;  // RuntimeException!

        // SAFE: Check first
        if (mystery instanceof Dog) {
            Dog dog = (Dog) mystery;
            dog.fetch();  // Now safe to call Dog methods
        }

        System.out.println("\n=== Pattern Matching (Java 16+) ===");

        Animal animal = new Cat("Luna");

        // Old way
        if (animal instanceof Cat) {
            Cat cat = (Cat) animal;
            cat.scratch();
        }

        // New way: Pattern matching
        if (animal instanceof Cat c) {
            c.scratch();  // c is already Cat type!
        }

        System.out.println("\n=== Calling Type-Specific Methods ===");

        for (Animal a : animals) {
            a.makeSound();  // Works for all

            // Type-specific behavior
            if (a instanceof Dog d) {
                d.fetch();
            } else if (a instanceof Cat c) {
                c.scratch();
            } else if (a instanceof Bird b) {
                b.fly();
            }
        }
    }
}

class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    void makeSound() {
        System.out.println(name + " makes a sound");
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + " barks!");
    }

    void fetch() {
        System.out.println(name + " fetches the ball!");
    }
}

class Cat extends Animal {
    Cat(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + " meows!");
    }

    void scratch() {
        System.out.println(name + " scratches!");
    }
}

class Bird extends Animal {
    Bird(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + " chirps!");
    }

    void fly() {
        System.out.println(name + " flies away!");
    }
}

// Type Checking with instanceof

public class Instanceof {
    public static void main(String[] args) {
        System.out.println("=== instanceof Operator ===\n");

        // Create various animals
        Animal[] animals = {
            new Dog("Buddy"),
            new Cat("Whiskers"),
            new Dog("Max"),
            new Bird("Tweety")
        };

        // Check types
        for (Animal animal : animals) {
            System.out.print(animal.name + ": ");

            if (animal instanceof Dog) {
                System.out.println("is a Dog");
            } else if (animal instanceof Cat) {
                System.out.println("is a Cat");
            } else if (animal instanceof Bird) {
                System.out.println("is a Bird");
            }
        }

        System.out.println("\n=== Safe Downcasting ===");

        Animal mystery = ;

        // UNSAFE: Could throw ClassCastException
        // Cat cat = (Cat) mystery;  // RuntimeException!

        // SAFE: Check first
        if (mystery instanceof Dog) {
            Dog dog = (Dog) mystery;
            dog.fetch();  // Now safe to call Dog methods
        }

        System.out.println("\n=== Pattern Matching (Java 16+) ===");

        Animal animal = new Cat("Luna");

        // Old way
        if (animal instanceof Cat) {
            Cat cat = (Cat) animal;
            cat.scratch();
        }

        // New way: Pattern matching
        if (animal instanceof Cat c) {
            c.scratch();  // c is already Cat type!
        }

        System.out.println("\n=== Calling Type-Specific Methods ===");

        for (Animal a : animals) {
            a.makeSound();  // Works for all

            // Type-specific behavior
            if (a instanceof Dog d) {
                d.fetch();
            } else if (a instanceof Cat c) {
                c.scratch();
            } else if (a instanceof Bird b) {
                b.fly();
            }
        }
    }
}

class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    void makeSound() {
        System.out.println(name + " makes a sound");
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + " barks!");
    }

    void fetch() {
        System.out.println(name + " fetches the ball!");
    }
}

class Cat extends Animal {
    Cat(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + " meows!");
    }

    void scratch() {
        System.out.println(name + " scratches!");
    }
}

class Bird extends Animal {
    Bird(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + " chirps!");
    }

    void fly() {
        System.out.println(name + " flies away!");
    }
}

// Type Checking with instanceof

public class Instanceof {
    public static void main(String[] args) {
        System.out.println("=== instanceof Operator ===\n");

        // Create various animals
        Animal[] animals = {
            new Dog("Buddy"),
            new Cat("Whiskers"),
            new Dog("Max"),
            new Bird("Tweety")
        };

        // Check types
        for (Animal animal : animals) {
            System.out.print(animal.name + ": ");

            if (animal instanceof Dog) {
                System.out.println("is a Dog");
            } else if (animal instanceof Cat) {
                System.out.println("is a Cat");
            } else if (animal instanceof Bird) {
                System.out.println("is a Bird");
            }
        }

        System.out.println("\n=== Safe Downcasting ===");

        Animal mystery = ;

        // UNSAFE: Could throw ClassCastException
        // Cat cat = (Cat) mystery;  // RuntimeException!

        // SAFE: Check first
        if (mystery instanceof Dog) {
            Dog dog = (Dog) mystery;
            dog.fetch();  // Now safe to call Dog methods
        }

        System.out.println("\n=== Pattern Matching (Java 16+) ===");

        Animal animal = new Cat("Luna");

        // Old way
        if (animal instanceof Cat) {
            Cat cat = (Cat) animal;
            cat.scratch();
        }

        // New way: Pattern matching
        if (animal instanceof Cat c) {
            c.scratch();  // c is already Cat type!
        }

        System.out.println("\n=== Calling Type-Specific Methods ===");

        for (Animal a : animals) {
            a.makeSound();  // Works for all

            // Type-specific behavior
            if (a instanceof Dog d) {
                d.fetch();
            } else if (a instanceof Cat c) {
                c.scratch();
            } else if (a instanceof Bird b) {
                b.fly();
            }
        }
    }
}

class Animal {
    String name;

    Animal(String name) {
        this.name = name;
    }

    void makeSound() {
        System.out.println(name + " makes a sound");
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + " barks!");
    }

    void fetch() {
        System.out.println(name + " fetches the ball!");
    }
}

class Cat extends Animal {
    Cat(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + " meows!");
    }

    void scratch() {
        System.out.println(name + " scratches!");
    }
}

class Bird extends Animal {
    Bird(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + " chirps!");
    }

    void fly() {
        System.out.println(name + " flies away!");
    }
}

if (animal instanceof Dog) checks real type before casting.

instanceof Check object's actual type: `obj instanceof Type`. Returns boolean.

Exercise: Practical.java

Build a payment processing system with polymorphism