OOP Intermediate
Polymorphism
Many Forms
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 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.
Parent reference to child
Store different subtypes in parent-type variable.
// 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.
// 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.
Arrays of polymorphic objects
Store different subtypes in one array.
// 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.
// 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.
Exercise: Practical.java
Build a payment processing system with polymorphism