Polymorphism is the core of OOP, allowing the same message (method call) to perform different actions depending on the type of object. Various child objects are stored in parent type variables, and the appropriate implementation is automatically selected at the time of call.
Learning Objectives
- Understand the meaning of upcasting (child → parent) and downcasting (parent → child)
- Uses JDK 16+
instanceofpattern matching grammar - Define an incomplete parent with the
abstractclass. - Understand the difference between static and dynamic types.
Key concepts
1) Upcasting and downcasting
Animal a = new Dog("Baduki"); //Upcasting (automatic)
Dog d = (Dog) a; //Downcasting (explicit)
Upcasting is always safe. For downcasting, the actual type must match, otherwise ClassCastException is displayed.
2) instanceof pattern matching
if (a instanceof Dog d) {
d.bark(); //variable already cast
}
Pattern matching in JDK 16+ involves type checking + casting + new variable declaration all at once.
3) abstract class
public abstract class Shape {
public abstract double area(); //no text
public void describe() {
System.out.println("area=" + area());
}
}
- If there is even one
abstractmethod without a body, the class itself must also beabstract. - Cannot be created directly with
new— must be implemented by a child - Can mix general methods and abstract methods (interfaces and differences)
4) Static type vs dynamic type
Animal a = new Dog("Baduki");
//Static type: Animal, dynamic type: Dog
a.speak(); //Dog.speak() is called (dynamic dispatch)
See as example
Example 1 — Polymorphism.java : Up/downcasting
public class Polymorphism {
static class Animal {
void speak() { System.out.println("..."); }
}
static class Cat extends Animal {
@Override void speak() { System.out.println("Meow"); }
void purr() { System.out.println("purr"); }
}
public static void main(String[] args) {
Animal a = new Cat(); //Upcasting
a.speak(); //Meow (Dynamic Dispatch)
//a.purr(); // Compilation error: None in Animal
Cat c = (Cat) a; //Downcasting
c.purr();
}
}
Execution result
Meow
purr
Note: Since the parent type does not know about the child's additional methods, child-specific calls require downcasting.
Example 2 — InstanceOfPattern.java: Pattern matching
public class InstanceOfPattern {
sealed interface Animal permits Dog, Cat, Fish {}
record Dog(String name) implements Animal {}
record Cat(String name) implements Animal {}
record Fish(String name) implements Animal {}
static String introduce(Animal a) {
if (a instanceof Dog d) return d.name() + "a puppy";
if (a instanceof Cat c) return c.name() + "is a cat";
if (a instanceof Fish f) return f.name() + "is a fish";
return "?";
}
public static void main(String[] args) {
Animal[] zoo = { new Dog("Baduki"), new Cat("butterfly"), new Fish("nemo") };
for (Animal a : zoo) System.out.println(introduce(a));
}
}
Execution result
dog playing baduk
butterfly is a cat
nemo is a fish
Note: Combining sealed with record is safe as it allows you to explicitly limit the list of children.
Example 3 — AbstractShape.java: Abstract class
public class AbstractShape {
static abstract class Shape {
abstract double area();
void describe() { System.out.println("area=" + area()); }
}
static class Square extends Shape {
double side;
Square(double s) { this.side = s; }
@Override double area() { return side * side; }
}
static class Circle extends Shape {
double r;
Circle(double r) { this.r = r; }
@Override double area() { return Math.PI * r * r; }
}
public static void main(String[] args) {
//new Shape(); // compile error: abstract
Shape[] xs = { new Square(2), new Circle(3) };
for (Shape s : xs) s.describe();
}
}
Execution result
area=4.0
area=28.274333882308138
Memo: Abstract classes are suitable for combining common behavior + blank space for children to fill.
Example 4 — SwitchPattern.java: switch pattern matching (JDK 21)
public class SwitchPattern {
sealed interface Pet permits DogPet, CatPet {}
record DogPet(String name, int age) implements Pet {}
record CatPet(String name) implements Pet {}
static String describe(Pet p) {
return switch (p) {
case DogPet d -> d.name() + "(" + d.age() + "flesh)";
case CatPet c -> c.name();
};
}
public static void main(String[] args) {
System.out.println(describe(new DogPet("Baduki", 3)));
System.out.println(describe(new CatPet("butterfly")));
}
}
Execution result
Baduki (3 years old)
butterfly
Note: When the patterns sealed and switch are encountered, the compiler verifies that all cases have been handled.
Common Mistakes
- Skip
instanceofcheck before downcasting →ClassCastException - Missing
@Overridemakes it a new method, not an overriding - Write the body in the
abstractmethod - Try to instantiate the abstract class directly as
new - Forgetting the constraint that
sealedchildren must have the same module/package
Cleanup
- Containing child objects in a parent type is the starting point of polymorphism.
instanceofpattern matching greatly improves readabilityabstractandsealedare useful for creating “only allowed children”
Try it yourself
cd 02_Object-Oriented/09_Polymorphism/src
javac Polymorphism.java
java Polymorphism