Polymorphism in Java – Compile-Time and Runtime Polymorphism
Polymorphism, from the Greek for "many forms", is the ability of one name to stand for different behaviour depending on context. In Java, the same method call can run different code depending on the arguments (compile-time polymorphism) or on the actual object it is called on (runtime polymorphism).
Polymorphism is what lets you write code against a general type such as Payment or List and have it work correctly with every specific type. This tutorial explains both kinds with runnable examples, along with upcasting, downcasting and the things that are not polymorphic.
1. Types of Polymorphism in Java
| Aspect | Compile-time (static) | Runtime (dynamic) |
|---|---|---|
| Achieved by | Method overloading | Method overriding |
| Method chosen | By the compiler, from argument types | By the JVM, from the actual object's class |
| Also called | Static binding, early binding | Dynamic method dispatch, late binding |
| Requires inheritance | No | Yes (class or interface) |
2. Compile-Time Polymorphism: Overloading
Several methods share a name but differ in their parameter lists. The compiler chooses one based on the arguments:
public class CompileTimePolymorphism {
// Same name, different parameter lists: the compiler picks the method
static int area(int side) { return side * side; }
static int area(int length, int width) { return length * width; }
static double area(double radius) { return Math.PI * radius * radius; }
public static void main(String[] args) {
System.out.println("Square: " + area(5));
System.out.println("Rectangle: " + area(4, 6));
System.out.printf("Circle: %.2f%n", area(2.0));
// println itself is overloaded: println(int), println(String), println(Object), ...
}
}
Square: 25 Rectangle: 24 Circle: 12.57
See Method Overloading for the full rules.
3. Runtime Polymorphism: Overriding and Dynamic Dispatch
A subclass overrides a method of its parent. When the method is called through a parent-type reference, the JVM runs the version belonging to the actual object:
import java.util.List;
public class RuntimePolymorphism {
static class Payment {
double fee(double amount) { return 0; }
String type() { return "Generic"; }
}
static class CardPayment extends Payment {
@Override double fee(double amount) { return amount * 0.02; }
@Override String type() { return "Card"; }
}
static class UpiPayment extends Payment {
@Override double fee(double amount) { return 0; }
@Override String type() { return "UPI"; }
}
static class WalletPayment extends Payment {
@Override double fee(double amount) { return Math.min(10, amount * 0.01); }
@Override String type() { return "Wallet"; }
}
public static void main(String[] args) {
// Reference type: Payment. Object type: decided at run time.
List<Payment> payments = List.of(new CardPayment(), new UpiPayment(), new WalletPayment());
for (Payment p : payments) {
System.out.printf("%-6s fee on 2000 = %.2f%n", p.type(), p.fee(2000));
}
}
}
Card fee on 2000 = 40.00 UPI fee on 2000 = 0.00 Wallet fee on 2000 = 10.00
The loop knows nothing about cards or wallets. Adding a new NetBankingPayment class requires no change to it, which is the open/closed principle in action. See Method Overriding.
4. Upcasting and Downcasting
Upcasting (child object to parent reference) is automatic and safe. The reference type limits which methods you can call; the object type decides which version runs. Downcasting (back to the child type) needs an explicit cast and is safe only if the object really is of that type:
public class UpcastingDowncasting {
static class Animal {
void sound() { System.out.println("Some sound"); }
}
static class Dog extends Animal {
@Override void sound() { System.out.println("Woof"); }
void fetch() { System.out.println("Dog fetches the ball"); }
}
static class Cat extends Animal {
@Override void sound() { System.out.println("Meow"); }
}
public static void main(String[] args) {
Animal a = new Dog(); // upcasting: automatic and always safe
a.sound(); // Woof (object type decides)
// a.fetch(); // compile error: Animal has no fetch()
if (a instanceof Dog d) { // safe downcast with pattern matching (Java 16+)
d.fetch();
}
Animal c = new Cat();
try {
Dog wrong = (Dog) c; // compiles, but fails at run time
} catch (ClassCastException e) {
System.out.println("ClassCastException: a Cat is not a Dog");
}
}
}
Woof Dog fetches the ball ClassCastException: a Cat is not a Dog
Use instanceof (with pattern matching since Java 16) before downcasting. Frequent downcasting is often a sign that a method should be added to the parent type instead.
5. What Is Not Polymorphic
Only overridable instance methods are dispatched dynamically. Fields, static methods and private methods are resolved by the reference type at compile time:
public class FieldsNotPolymorphic {
static class Parent {
String name = "Parent field";
static String id() { return "Parent static"; }
String describe() { return "Parent method"; }
}
static class Child extends Parent {
String name = "Child field"; // hides, does not override
static String id() { return "Child static"; } // hides, does not override
@Override String describe() { return "Child method"; }
}
public static void main(String[] args) {
Parent p = new Child();
System.out.println(p.describe()); // instance method: overridden -> Child
System.out.println(p.name); // field: chosen by reference type -> Parent
System.out.println(Parent.id()); // static: chosen at compile time
}
}
Child method Parent field Parent static
- Fields are hidden, not overridden.
- Static methods are hidden, not overridden.
- Private and final methods cannot be overridden, so they are bound statically.
- Constructors are not inherited and therefore not polymorphic.
6. Polymorphism with Interfaces and Sealed Types
Polymorphism works equally with interfaces: a List<String> variable may hold an ArrayList or a LinkedList. Modern Java adds another style: sealed types with pattern-matching switch, where the compiler checks that every subtype is handled:
public class SealedPolymorphism {
sealed interface Shape permits Circle, Square, Rect { }
record Circle(double r) implements Shape { }
record Square(double side) implements Shape { }
record Rect(double w, double h) implements Shape { }
// Pattern matching switch: exhaustive because Shape is sealed (Java 21)
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Square q -> q.side() * q.side();
case Rect r -> r.w() * r.h();
};
}
public static void main(String[] args) {
Shape[] shapes = { new Circle(1), new Square(3), new Rect(2, 5) };
for (Shape s : shapes) System.out.printf("%-25s area = %.2f%n", s, area(s));
}
}
Circle[r=1.0] area = 3.14 Square[side=3.0] area = 9.00 Rect[w=2.0, h=5.0] area = 10.00
Overriding puts behaviour in each class; pattern matching puts it in one function. The first makes adding types easy, the second makes adding operations easy.
7. Benefits of Polymorphism
- Extensibility: new subclasses work with existing code.
- Simpler code: one loop or method handles many types; no long
if-elsechains on type. - Loose coupling: code depends on abstractions, not concrete classes.
- Reusability: generic algorithms (sorting, collections) work with any compatible type.
8. Summary
- Polymorphism means one interface, many implementations.
- Compile-time polymorphism uses overloading; the compiler picks the method from argument types.
- Runtime polymorphism uses overriding; the JVM picks the method from the object's actual class.
- Upcasting is automatic; downcasting needs a cast and an
instanceofcheck. - Fields, static, private and final methods are not dynamically dispatched.
Related Tutorials
Frequently Asked Questions (FAQ)
Q1. What is polymorphism in Java?
The ability of a method call or reference to behave differently depending on argument types (overloading) or on the actual object type (overriding).
Q2. What are the types of polymorphism in Java?
Compile-time (static) polymorphism through method overloading, and runtime (dynamic) polymorphism through method overriding.
Q3. What is dynamic method dispatch?
The mechanism by which the JVM selects an overridden method's implementation at run time based on the actual object, not the reference type.
Q4. Is method overloading true polymorphism?
It is usually classed as compile-time (ad hoc) polymorphism. Some authors reserve "polymorphism" for runtime dispatch, but both are commonly accepted in Java interviews.
Q5. What is upcasting?
Assigning a subclass object to a superclass or interface reference, such as Animal a = new Dog();. It is implicit and always safe.
Q6. What is downcasting?
Casting a parent reference back to a subclass type, like (Dog) a. It throws ClassCastException if the object is not actually of that type.
Q7. Can static methods be polymorphic?
No. Static methods are resolved at compile time using the reference type. A subclass static method with the same signature hides the parent's.
Q8. Are fields polymorphic in Java?
No. Field access depends on the reference type. A subclass field with the same name hides the parent field.
Q9. Can private methods be overridden?
No. They are not visible to subclasses; a same-named method in a subclass is a new, unrelated method.
Q10. Can constructors be polymorphic?
Constructors can be overloaded but not overridden, because they are not inherited.
Q11. What is the difference between static and dynamic binding?
Static binding resolves the method at compile time (overloaded, static, private and final methods). Dynamic binding resolves it at run time (overridden instance methods).
Q12. What is a real-world example of polymorphism?
A payment system calls pay() on any payment method, and cards, UPI and wallets each process it their own way. A person can also be a student, an employee and a customer in different contexts.
Q13. Is polymorphism possible without inheritance?
Overloading needs no inheritance. Runtime polymorphism needs a common supertype, which can be a class or an interface.
Q14. What is parametric polymorphism?
Writing code that works with any type through type parameters. In Java this is generics, as in List<T>.
Q15. Why is polymorphism useful?
It lets code work with general types, so new implementations can be added without changing existing code, reducing conditionals and coupling.
Q16. Does polymorphism work with interfaces?
Yes. A reference of an interface type can point to any implementing object, and calls go to that object's implementation.
Q17. Can final methods take part in runtime polymorphism?
A final method cannot be overridden, so there is only one implementation, and calls to it are effectively bound statically.
Q18. Does Java support operator overloading?
No, apart from built-in cases such as + for both numeric addition and string concatenation. You cannot define your own operator overloads.
Q19. Which method runs when an overridden method is called from a constructor?
The subclass version, even though the subclass's fields may not be initialised yet. This is why constructors should not call overridable methods.
Q20. How does pattern matching relate to polymorphism?
Pattern-matching switch over sealed types is an alternative to overriding: one method handles each subtype explicitly, and the compiler checks that every case is covered.