In this beginner-friendly guide, you will learn Java polymorphism compile-time vs runtime examples with clear code you can run today. If you are a student or new to Java OOP, this tutorial explains what polymorphism is, why it matters, and how to use method overloading and overriding step by step. By the end, you will know the difference between compile time and runtime polymorphism in Java, how to avoid common mistakes, and how modern Java (21+) makes polymorphism safer and easier.
What is polymorphism in Java? (For beginners)
Polymorphism means “many forms.” In Java OOP, it lets the same method name behave differently depending on its input or the actual object type. In simple words: you write one interface or method name, and Java decides the correct behavior based on context.
- Compile-time polymorphism (method/constructor overloading): The compiler selects a method based on parameter types and count. Decision happens before the program runs.
- Runtime polymorphism (method overriding): The JVM selects which overridden instance method to call based on the object’s real type at runtime (dynamic dispatch).
Why use polymorphism? It reduces duplication, keeps your code flexible, and lets you “program to an interface.” This is essential for building clean, testable Java applications.
Compile-time polymorphism in Java (method overloading)
Method overloading happens when a class has multiple methods with the same name but different parameter lists. The compiler decides which overload to call based on the argument types and number. Return type alone does not distinguish overloads.
Simple overloading example
class Printer {
void print(int n) {
System.out.println("Printing int: " + n);
}
void print(String s) {
System.out.println("Printing String: " + s);
}
void print(double d, int times) {
System.out.println("Printing double " + d + ", " + times + " times:");
for (int i = 0; i < times; i++) {
System.out.print(d + " ");
}
System.out.println();
}
}
class="cd-keyword cd-access">public class OverloadDemo {
class="cd-keyword cd-access">public static void main(String[] args) {
Printer p = new Printer();
p.print(5); // calls print(int)
p.print("Hello"); // calls print(String)
p.print(3.14, 3); // calls print(double, int)
// char promotes to int; the most specific overload is chosen
p.print('A'); // calls print(int) and prints its numeric value 65
}
}Printing int: 5
Printing String: Hello
Printing double 3.14, 3 times:
3.14 3.14 3.14
Printing int: 65Constructor overloading
Constructors can be overloaded too (different parameter lists). Constructors are not inherited and cannot be overridden.
class Student {
String name;
int age;
Student(String name) {
this.name = name;
this.age = 0;
}
Student(String name, int age) {
this.name = name;
this.age = age;
}
void info() {
System.out.println(name + " (" + age + ")");
}
}
class="cd-keyword cd-access">public class ConstructorOverload {
class="cd-keyword cd-access">public static void main(String[] args) {
Student a = new Student("Anita");
Student b = new Student("Rohan", 19);
a.info();
b.info();
}
}Anita (0)
Rohan (19)How the compiler chooses an overload
- Match by exact parameter types first.
- Then by widening conversions if needed (e.g., char to int, int to long).
- Autoboxing/unboxing may apply (e.g., int to Integer).
- Most specific applicable method wins.
- Return type is ignored during overload resolution.
Common overloading pitfalls
- Ambiguous calls: similar overloads can confuse the compiler.
- Expecting return type to distinguish overloads (it does not).
class Ambiguity {
void test(Integer x) { System.out.println("Integer"); }
void test(Long x) { System.out.println("Long"); }
class="cd-keyword cd-access">public static void main(String[] args) {
Ambiguity a = new Ambiguity();
// a.test(null); // Compile error: reference to test is ambiguous
a.test((Integer) null); // OK: picks Integer overload
}
}Runtime polymorphism in Java (method overriding)
Method overriding happens when a subclass provides its own implementation of an inherited instance method with the same signature. At runtime, Java picks the method based on the object’s actual type, not the reference type. This is called dynamic dispatch and is the core of runtime polymorphism.
Simple runtime polymorphism example using inheritance
class Animal {
void speak() {
System.out.println("Animal speaks");
}
}
class Dog extends Animal {
class="cd-annotation">@Override
void speak() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
class="cd-annotation">@Override
void speak() {
System.out.println("Cat meows");
}
}
class="cd-keyword cd-access">public class RuntimePolymorphismDemo {
class="cd-keyword cd-access">public static void main(String[] args) {
Animal a1 = new Dog(); // upcasting
Animal a2 = new Cat();
a1.speak(); // Dog barks (decided at runtime)
a2.speak(); // Cat meows
Animal[] animals = { new Dog(), new Cat(), new Animal() };
for (Animal a : animals) {
a.speak();
}
}
}Dog barks
Cat meows
Dog barks
Cat meows
Animal speaksRules of overriding (what beginners should remember)
- Signatures must match: same method name and parameter list.
- Use
@Overrideto catch mistakes early. - Return types can be covariant (subtype of the original return type).
- Access level cannot be reduced (e.g., public cannot become private).
- final instance methods cannot be overridden.
- private methods are not inherited and cannot be overridden.
- static methods are not overridden; they are hidden and resolved at compile time.
class Base {
void greet() {
System.out.println("Base.greet (instance)");
}
static void who() {
System.out.println("Base.who (static)");
}
}
class Sub extends Base {
class="cd-annotation">@Override
void greet() {
System.out.println("Sub.greet (instance)");
}
static void who() {
System.out.println("Sub.who (static)");
}
}
class="cd-keyword cd-access">public class StaticVsOverrideDemo {
class="cd-keyword cd-access">public static void main(String[] args) {
Base ref = new Sub();
// Dynamic dispatch for instance methods:
ref.greet(); // Sub.greet (instance)
// Static methods are resolved by reference type or class name:
Base.who(); // Base.who (static)
Sub.who(); // Sub.who (static)
// The following is allowed but not recommended; it calls Base.who()
ref.who(); // Base.who (static)
}
}Sub.greet (instance)
Base.who (static)
Sub.who (static)
Base.who (static)Method overloading vs overriding (quick comparison)
| Aspect | Compile-time polymorphism (Overloading) | Runtime polymorphism (Overriding) |
|---|---|---|
| Decision time | At compile time (by the compiler) | At runtime (by the JVM via dynamic dispatch) |
| Where it happens | Within the same class | Between superclass and subclass |
| Signature rule | Same name, different parameter list | Same name and parameter list |
| Return type | Does not distinguish overloads | Can be covariant (subtype of original) |
| Static methods | Can be overloaded | Not applicable (static methods are hidden, not overridden) |
Overriding: higher
Overriding: medium
Overriding: lower
Overriding: small
Overloading (compile-time)
Overriding (runtime)
When should a beginner use polymorphism in Java and why?
- When multiple classes share behavior but differ in details (e.g.,
Animalsubclasses speaking differently). - When you want to code to an interface and swap implementations easily (e.g.,
Listcan beArrayListorLinkedList). - When you need cleaner, testable design: pass abstractions, not concrete classes.
- Use overloading for convenience APIs; use overriding for flexible, extensible behavior.
- If you changed the parameter list, you created an overload (compile-time).
- If you kept the same signature in a subclass and changed the body, you made an override (runtime).
- Always add
@Overridein subclasses to catch mistakes early. - Do not expect return type alone to differentiate overloads—Java ignores it for resolution.
- Call static methods via the class name, not an object reference (no dynamic dispatch for statics).
- For ambiguous overloads (e.g.,
test(Integer)vstest(Long)), cast the argument explicitly. - When overriding, you cannot reduce visibility and you should not throw broader checked exceptions than the superclass method.
- Program to interfaces for runtime polymorphism: e.g., declare
List<String> list = new ArrayList<>();
Modern Java tip: safer runtime polymorphism with sealed classes and pattern matching (Java 21+)
Java 17 introduced sealed classes and interfaces, and Java 21 added pattern matching for switch. Together, they make subtype polymorphism more explicit and safe for students. You restrict which subclasses are allowed, and you can switch over them exhaustively.
sealed interface Shape permits Circle, Rectangle, Square {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}
record Square(double side) implements Shape {}
class="cd-keyword cd-access">public class ShapeDemo {
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Square sq -> sq.side() * sq.side();
};
}
class="cd-keyword cd-access">public static void main(String[] args) {
Shape[] shapes = {
new Circle(2),
new Rectangle(3, 4),
new Square(5)
};
for (Shape s : shapes) {
System.out.println(s.getClass().getSimpleName() + " area = " + area(s));
}
}
}Circle area = 12.566370614359172
Rectangle area = 12.0
Square area = 25.0This approach avoids long if (s instanceof ...) chains and ensures you handle all permitted subtypes. It’s available in Java 21 and later.
Common mistakes with polymorphism in Java for learners
- Thinking overloading is runtime polymorphism. It is compile-time.
- Forgetting that return type alone cannot distinguish overloaded methods.
- Calling static methods via an object reference and expecting dynamic dispatch. Static methods are resolved at compile time by the reference type.
- Trying to override
privateorfinalmethods. Private methods aren’t inherited; final methods can’t be overridden. - Changing method parameters when attempting to override, which actually creates an overload instead of an override. Use
@Overrideto catch this. - Narrowing access modifier in subclass (e.g., public to protected). That’s not allowed.
- Forgetting that constructors aren’t inherited and cannot be overridden—only overloaded.
Practice ideas for students
- Write a
Calculatorwith overloadedsummethods:sum(int, int),sum(double, double),sum(int, int, int). - Create a
Vehiclebase class and override amove()method inCar,Bike, andBus. Store them in aVehicle[]and callmove(). - Model shapes with a sealed interface and use a pattern matching
switchto compute perimeters.
FAQ: Java polymorphism tutorial for students
What is polymorphism in Java in simple words?
It means the same method name can do different things depending on input types (overloading) or the real object type (overriding). It helps write flexible, reusable code.
How is compile-time polymorphism different from runtime polymorphism in Java?
Compile-time polymorphism (overloading) is decided by the compiler using method signatures. Runtime polymorphism (overriding) is decided when the program runs, based on the object’s actual type.
How do method overloading and overriding show polymorphism in Java?
- Overloading: same method name, different parameters; the compiler chooses the best match.
- Overriding: subclass replaces a superclass method; the JVM calls the subclass’s method at runtime through dynamic dispatch.
Can you give a basic example of runtime polymorphism using inheritance?
Yes: Animal a = new Dog(); a.speak(); will call Dog’s speak() at runtime, even though the reference type is Animal. See the “Runtime Polymorphism” example above.
When should a beginner use polymorphism in Java and why?
Use it when multiple types share a common behavior but differ in implementation. It makes your code easier to extend and test—just add a new subclass or implementation without changing existing logic.
Sources / Further reading
- Oracle: Polymorphism — The Java Tutorials: https://docs.oracle.com/javase/tutorial/java/IandI/polymorphism.html
- Java Language Specification (JLS), Java SE 26: https://docs.oracle.com/javase/specs/jls/se26/html/
- JEP 441: Pattern Matching for switch (Java 21): https://openjdk.org/jeps/441
- Sealed Classes and Interfaces (Java 17 JVMS): https://docs.oracle.com/en/java/javase/17/docs/specs/sealed-classes-jvms.html
- Using Pattern Matching — dev.java: https://dev.java/learn/pattern-matching/
Keep exploring Java OOP, collections, exceptions, and more in our Java section: https://coddesire.com/java/


