Polymorphism — from the Greek for “many forms” — is one of the four pillars of Object-Oriented Programming in Java. It lets a single method name behave differently depending on the arguments it receives or the object it is called on. Java supports two types: compile-time polymorphism (method overloading, resolved by the compiler) and runtime polymorphism (method overriding, resolved dynamically at run time). In this post we look at hands-on examples of both, starting with the simpler overloading form and then showing how overriding works through inheritance.
Part 1: Compile-Time Polymorphism (Method Overloading)
Method overloading means defining multiple methods in the same class with the same name but different parameter lists. The compiler decides which version to call based on the number or types of arguments at compile time. The example below defines two calculateArea() methods — one for a circle (one parameter) and one for a rectangle (two parameters).
import java.io.*;
class ShapeCalculator {
// Overloaded method 1: calculates area of a circle given its radius
public static double calculateArea(int radius) {
return Math.PI * radius * radius; // PI * r^2
}
// Overloaded method 2: calculates area of a rectangle given width and height
public static int calculateArea(int width, int height) {
return width * height; // w * h
}
public static void main(String args[]) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("Choose shape:");
System.out.println("1. Circle");
System.out.println("2. Rectangle");
int choice = Integer.parseInt(reader.readLine());
switch (choice) {
case 1: {
System.out.println("Enter radius of circle:");
int radius = Integer.parseInt(reader.readLine());
// Compiler picks calculateArea(int) because one argument is passed
System.out.printf("Area of circle = %.2f%n", calculateArea(radius));
break;
}
case 2: {
System.out.println("Enter width of rectangle:");
int width = Integer.parseInt(reader.readLine());
System.out.println("Enter height of rectangle:");
int height = Integer.parseInt(reader.readLine());
// Compiler picks calculateArea(int, int) because two arguments are passed
System.out.println("Area of rectangle = " + calculateArea(width, height));
break;
}
default:
System.out.println("Invalid choice.");
}
}
}
How the Overloading Code Works
- Same method name, different signatures — Both
calculateArea(int radius)andcalculateArea(int width, int height)share the same name. The compiler differentiates them purely by the number of parameters. - Circle branch (case 1) — The user enters a radius. Calling
calculateArea(radius)with one argument causes the compiler to select the single-parameter version, which applies the formulaπr². - Rectangle branch (case 2) — The user enters width and height. Calling
calculateArea(width, height)with two arguments causes the compiler to select the two-parameter version, which returnswidth × height. - Resolved at compile time — The binding between the call site and the correct method is fixed before the program runs. This is why overloading is called compile-time (or static) polymorphism.
Sample Output — Overloading
Choose shape:
1. Circle
2. Rectangle
1
Enter radius of circle:
5
Area of circle = 78.54
Choose shape:
1. Circle
2. Rectangle
2
Enter width of rectangle:
5
Enter height of rectangle:
10
Area of rectangle = 50
Part 2: Runtime Polymorphism (Method Overriding)
Method overriding occurs when a subclass provides its own implementation of a method already defined in its parent class. The correct version is chosen at run time based on the actual type of the object, not the reference type. This requires inheritance and the @Override annotation is strongly recommended as a safety check.
// Base class
class Shape {
// Default describe() — subclasses will override this
public void describe() {
System.out.println("I am a generic shape.");
}
}
// Subclass 1: overrides describe() with circle-specific behaviour
class Circle extends Shape {
private int radius;
Circle(int radius) {
this.radius = radius;
}
@Override
public void describe() {
System.out.println("I am a Circle with radius " + radius +
" and area = " + String.format("%.2f", Math.PI * radius * radius));
}
}
// Subclass 2: overrides describe() with rectangle-specific behaviour
class Rectangle extends Shape {
private int width;
private int height;
Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public void describe() {
System.out.println("I am a Rectangle " + width + "x" + height +
" with area = " + (width * height));
}
}
// Driver class
class ShapeDemo {
public static void main(String args[]) {
// Parent reference pointing to different child objects
Shape shape1 = new Circle(5);
Shape shape2 = new Rectangle(4, 6);
Shape shape3 = new Shape();
// The JVM decides at run time which describe() to call
shape1.describe(); // calls Circle's version
shape2.describe(); // calls Rectangle's version
shape3.describe(); // calls Shape's version
}
}
How the Overriding Code Works
- Parent class
Shape— Defines adescribe()method with generic output. Any subclass that does not override it will fall back to this default. Circleoverridesdescribe()— Using@Override, theCirclesubclass provides its own version that prints the radius and computed area.Rectangleoverridesdescribe()— Similarly,Rectanglereplaces the parent’s implementation with one that prints dimensions and area.- Parent reference, child object — In
main(), all three variables are declared as typeShape, but two actually holdCircleandRectangleobjects. Java tracks the real type of the object at run time. - Dynamic dispatch — When
shape1.describe()is called, the JVM looks up the actual object type (Circle) and invokesCircle.describe()— notShape.describe(). This run-time lookup is the essence of runtime polymorphism.
Sample Output — Overriding
I am a Circle with radius 5 and area = 78.54
I am a Rectangle 4x6 with area = 24
I am a generic shape.
Output Explanation
- Line 1 —
shape1holds aCircleobject, so the JVM callsCircle.describe(), printing the radius and the calculated area (78.54 for radius 5). - Line 2 —
shape2holds aRectangleobject, soRectangle.describe()is invoked, printing dimensions 4×6 and area 24. - Line 3 —
shape3is a plainShapeobject with no override, so it falls through toShape.describe()and prints the generic message.
See Also
Conclusion
Polymorphism makes Java code flexible and extensible. Method overloading keeps related operations under a single, intuitive name while letting the compiler pick the right version based on arguments. Method overriding lets you write code against a parent type and have the correct subclass behaviour kick in automatically at run time — a pattern central to frameworks, design patterns, and clean architecture. Together, these two forms of polymorphism are among the most frequently used tools in everyday Java development.
What is the difference between method overloading and method overriding?
Overloading happens within the same class: multiple methods share a name but have different parameter lists, and the correct one is chosen at compile time. Overriding happens across a parent-child class pair: the child redefines a method from the parent with the same signature, and the correct version is chosen at run time based on the actual object type.
Can you overload a method by changing only its return type?
No. Changing only the return type is not sufficient to overload a method in Java. The compiler distinguishes overloaded methods by their parameter list (number, type, and order of parameters). If two methods in the same class have the same name and the same parameters but different return types, the compiler reports an error.
What is dynamic method dispatch?
Dynamic method dispatch is the mechanism Java uses to implement runtime polymorphism. When you call an overridden method on a parent-type reference, the JVM checks the actual (runtime) type of the object and calls the appropriate overridden version. This is what makes it possible to write code against an interface or parent class and have the correct subclass behaviour execute without knowing the concrete type at compile time.
Why should I use the @Override annotation?
@Override tells the compiler that you intend to override a parent method. If you accidentally misspell the method name or use a different parameter signature — meaning you are actually overloading instead of overriding — the compiler will flag it as an error immediately. Without @Override, the bug would be silent and very hard to trace at run time.
Can static methods be overridden in Java?
No. Static methods belong to the class, not to any instance. If a subclass defines a static method with the same signature as a static method in the parent, this is called method hiding, not overriding. The method that runs is determined by the reference type at compile time, not the actual object type at run time — so dynamic dispatch does not apply to static methods.