Polymorphism
Polymorphism ("many forms") means code written for a general type works with any of its subtypes, and each object responds in its own way. A list of Shapes can hold circles, squares and triangles; calling area() on each runs the right calculation. It's what makes object-oriented code extensible.
One variable type, many object types
class Notification { String send(String message) { return "Generic: " + message; } } class EmailNotification extends Notification { @Override String send(String message) { return "Email: " + message; } } class SmsNotification extends Notification { @Override String send(String message) { return "SMS: " + message.substring(0, Math.min(20, message.length())); } } public class Main { public static void main(String[] args) { Notification[] channels = {new EmailNotification(), new SmsNotification(), new Notification()}; for (Notification n : channels) { System.out.println(n.send("Your order has shipped today")); } } }
Email: Your order has shipped today SMS: Your order has shipp Generic: Your order has shipped today
The array's type is Notification, but each element is a specific subclass. When send is called, Java looks at the actual object at run time and runs its version. This is dynamic dispatch. Adding a new channel needs a new class, not changes to this loop.
Static type vs runtime type
class Animal { void speak() { System.out.println("..."); } } class Cat extends Animal { @Override void speak() { System.out.println("Meow"); } void purr() { System.out.println("Purr"); } } public class Main { public static void main(String[] args) { Animal a = new Cat(); a.speak(); a.purr(); } }
Main.java:14: error: cannot find symbol
The declared type (Animal) decides which methods you may call; the runtime type (Cat) decides which implementation runs. a.speak() prints "Meow", but a.purr() doesn't compile, because not every Animal can purr.
Upcasting and downcasting
class Animal { void speak() { System.out.println("..."); } } class Cat extends Animal { @Override void speak() { System.out.println("Meow"); } void purr() { System.out.println("Purr"); } } class Dog extends Animal { @Override void speak() { System.out.println("Woof"); } } public class Main { public static void main(String[] args) { Animal[] pets = {new Cat(), new Dog()}; for (Animal pet : pets) { pet.speak(); if (pet instanceof Cat cat) { cat.purr(); } } } }
Meow Purr Woof
Assigning a Cat to an Animal variable (upcasting) is automatic. Going back down needs a check: pet instanceof Cat cat tests the type and, if it matches, gives you a variable of the narrower type (pattern matching, Java 16+). A plain cast (Cat) pet on a Dog would throw ClassCastException.
instanceof checks often mean behaviour should be a method on the classes instead. Sealed types and pattern-matching switches (lesson 49) are the modern exception, when you want to handle a fixed set of types in one place.Polymorphic parameters
class Shape { double area() { return 0; } } class Circle extends Shape { double r; Circle(double r) { this.r = r; } @Override double area() { return Math.PI * r * r; } } class Rect extends Shape { double w, h; Rect(double w, double h) { this.w = w; this.h = h; } @Override double area() { return w * h; } } public class Main { static double totalArea(Shape... shapes) { double sum = 0; for (Shape s : shapes) sum += s.area(); return sum; } public static void main(String[] args) { System.out.println("%.2f".formatted(totalArea(new Circle(1), new Rect(2, 3), new Rect(1, 1)))); } }
10.14
totalArea works with every current and future kind of Shape. Writing methods against the most general type that offers what you need is a key principle of flexible code.
Overloading is not polymorphism
class Animal {} class Cat extends Animal {} public class Main { static String identify(Animal a) { return "an animal"; } static String identify(Cat c) { return "a cat"; } public static void main(String[] args) { Animal pet = new Cat(); System.out.println(identify(pet)); System.out.println(identify(new Cat())); } }
an animal a cat
Overloads are chosen at compile time from the declared type, so identify(pet) picks the Animal version even though the object is a cat. Only overridden methods dispatch on the runtime type.
Try it yourself
Calculate pay for different kinds of employees through one loop.
Mei 8166.67 Lucas 2220.00