Polymorphism in Java
One reference type, many possible behaviours. Polymorphism is what lets code work with a supertype and stay correct as new subtypes appear.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Definition
Polymorphism means a single reference type can refer to objects of many types, and the behaviour that runs depends on the actual object. Literally, one name with many forms.
The two kinds
| Compile time polymorphism | Runtime polymorphism | |
|---|---|---|
| Achieved by | Method overloading | Method overriding |
| Resolved | At compile time | At runtime |
| Based on | Declared argument types | The actual object |
| Also called | Static binding | Dynamic binding |
When people say "polymorphism" without qualification they almost always mean the runtime kind.
Runtime polymorphism in practice
abstract class Shape {
abstract double area();
}
class Circle extends Shape {
private final double radius;
Circle(double radius) { this.radius = radius; }
@Override double area() { return Math.PI * radius * radius; }
}
class Rectangle extends Shape {
private final double width;
private final double height;
Rectangle(double width, double height) { this.width = width; this.height = height; }
@Override double area() { return width * height; }
}List<Shape> shapes = List.of(new Circle(2), new Rectangle(3, 4));
double total = 0;
for (Shape shape : shapes) {
total += shape.area(); // the right implementation each time
}
System.out.printf("Total area: %.2f%n", total);The loop knows nothing about circles or rectangles. Add a Triangle tomorrow and this code does not change at all. That is the practical value of polymorphism: existing code keeps working as new types arrive.Upcasting and downcasting
Shape shape = new Circle(2); // upcast, implicit and always safe
// The compiler now only offers Shape members
// shape.radius; // not visible
if (shape instanceof Circle circle) { // test and cast in one step
System.out.println("A circle was supplied");
}Upcasting narrows what the compiler will let you call, but it never changes the object. The override still runs, which is the whole point.
Fields are not polymorphic
class Parent {
String label = "parent";
String describe() { return "parent method"; }
}
class Child extends Parent {
String label = "child";
@Override String describe() { return "child method"; }
}
Parent p = new Child();
System.out.println(p.label); // parent - fields use the declared type
System.out.println(p.describe()); // child method - methods use the objectField access is resolved at compile time and is hidden, not overridden. This is a common interview question, and it is also a good reason to keep fields private.
Polymorphism through interfaces
interface Exporter {
String export(Note note);
}
class JsonExporter implements Exporter {
@Override public String export(Note note) { return "{...}"; }
}
class CsvExporter implements Exporter {
@Override public String export(Note note) { return "id,title"; }
}
class ExportService {
private final Exporter exporter; // depends on the abstraction
ExportService(Exporter exporter) {
this.exporter = exporter;
}
String run(Note note) {
return exporter.export(note);
}
}The service is written against the interface, so the concrete exporter can be chosen at runtime and swapped in a test. Interface polymorphism is the form used most in real code, because it does not require a class hierarchy.
How the JVM does it
Each class has a table of method implementations. A virtual call looks up the method in the table belonging to the object, not the reference. Modern JVMs then optimise heavily: when only one implementation has ever been seen at a call site, the JIT compiler can inline it and remove the lookup entirely.
Common mistakes
- Expecting fields to behave like methods under polymorphism.
- Writing a chain of
instanceoftests instead of letting the objects decide. Each new type then means editing that chain. - Casting down to reach a subtype method, which usually means the abstraction is wrong.
- Assuming an overloaded method is chosen from the runtime type.
- Declaring variables as the concrete class instead of the interface.
Best practices
- Declare variables, parameters and return types using the most general type that suffices.
- Replace type checking chains with an overridden method, or with a sealed hierarchy and pattern matching where that reads better.
- Keep the supertype contract meaningful, so every subtype can honour it.
- Prefer interface polymorphism to inheritance when there is no shared state.
Practice
- Add a
Triangleto the shape example and confirm that the totalling loop needs no change. - Predict both lines of the field hiding example, then make the fields private and describe what changes.
- Rewrite an
if (x instanceof A) ... else if (x instanceof B)chain using an overridden method. - Why does
List<Shape> shapesallow both circles and rectangles, and what does the compiler permit you to call on an element? - Explain the difference between overloading and overriding using a single short program.
Conclusion
Polymorphism lets you write code once against a general type and have it stay correct as new specific types appear. Methods are dispatched on the object, fields are not, and interfaces are usually the cleanest way to obtain it.