Table of Contents
Polymorphism, derived from the Greek words poly (many) and morph (forms), is a core principle of object-oriented programming (OOP) that allows objects to take on multiple forms. In Java, polymorphism enables methods to perform different actions based on the object invoking them, fostering flexibility and code reusability.
In this article, we will break down how polymorphism works in Java, its types, and practical examples to solidify your understanding.
What is Polymorphism?
Polymorphism lets a single interface represent different underlying behaviors. For instance, a Dog
object and a Cat
object can both be treated as Animal
objects, yet each produces a unique sound when their makeSound()
method is called. This concept simplifies code design by allowing you to write generalized code that works with a broad range of related objects.
Types of Polymorphism in Java
Java implements polymorphism in two ways:
1. Compile-Time Polymorphism (Static)
- Achieved through method overloading.
- Methods share the same name but differ in parameters (type, number, or order).
- The correct method is chosen at compile time based on the method signature.
Example of Method Overloading:
The Calculator
class below demonstrates overloading with two add()
methods for integers and doubles:
class Calculator {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
}
Here, add()
works for both integers and doubles, determined by the arguments passed.
2. Runtime Polymorphism (Dynamic)
- Achieved through method overriding.
- A subclass provides a specific implementation of a method already defined in its superclass.
- The correct method is chosen at runtime based on the object’s actual type.
Example of Method Overriding:
In this example, Dog
and Cat
override the makeSound()
method of Animal
:
Here, myAnimal
is an Animal
reference pointing to a Dog
object. At runtime, the JVM invokes Dog
’s makeSound()
.
Key Concepts in Polymorphism
1. Inheritance & Method Overriding
Polymorphism relies on inheritance. A subclass overrides a superclass method to provide specialized behavior.
- Use the
@Override
annotation to ensure correct overriding (prevents typos or signature mismatches).
2. Reference vs. Object Type
- Reference Type: Determines which methods/fields are accessible.
- Object Type: Determines which method implementation is executed at runtime.
Example:
The code below shows a Cat
object referenced by an Animal
variable. The reference type is Animal
, but the object type is Cat
, so Cat
’s makeSound()
is called:
Animal myPet = new Cat();
myPet.makeSound(); // Output: Cat meows
3. Interfaces and Polymorphism
Java interfaces enable polymorphism by allowing multiple implementations.
Example:
The Shape
interface is implemented by Circle
and Square
, each defining their own draw()
method:
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() {
System.out.println("Drawing a circle");
}
}
class Square implements Shape {
public void draw() {
System.out.println("Drawing a square");
}
}
public class Main {
public static void main(String[] args) {
Shape shape = new Circle();
shape.draw(); // Output: Drawing a circle
}
}
Common Pitfalls

1. Static Methods: Bound to the Class, Not the Object
Static methods are associated with the class itself, not individual instances. They cannot be overridden in the traditional sense because they lack runtime polymorphism. Instead, a subclass can hide a superclass’s static method by declaring its own method with the same signature. The method invoked depends on the reference type, not the object type.
Example:
class Parent {
static void print() {
System.out.println("Parent's static method");
}
}
class Child extends Parent {
static void print() { // Hiding, NOT overriding
System.out.println("Child's static method");
}
}
public class Main {
public static void main(String[] args) {
Parent p = new Child();
p.print(); // Output: "Parent's static method" (uses reference type)
Child.print(); // Output: "Child's static method"
}
}
Why It Matters:
If you mistakenly try to “override” a static method, the subclass method will behave like an independent method tied to the subclass. This can lead to unexpected results if you expect dynamic dispatch (runtime polymorphism).
Best Practice:
- Use instance methods for true polymorphism.
- Avoid redefining static methods in subclasses unless intentionally hiding them.
2. Instance Variables: No Polymorphism
Instance variables are resolved at compile time based on the reference type, not the object’s actual type. Unlike methods, variables do not participate in overriding or runtime polymorphism.
Example:
class Vehicle {
String fuel = "Petrol";
}
class Car extends Vehicle {
String fuel = "Diesel"; // Shadows the superclass variable
}
public class Main {
public static void main(String[] args) {
Vehicle v = new Car();
System.out.println(v.fuel); // Output: "Petrol" (reference type determines access)
System.out.println(((Car) v).fuel); // Output: "Diesel" (cast to object type)
}
}
Why It Matters:
If a subclass redefines a superclass variable, accessing it through a superclass reference will use the superclass’s variable. This can cause confusion if you expect the subclass variable to “override” the parent’s.
Best Practice:
- Avoid reusing variable names in subclasses.
- Use getter/setter methods to enforce encapsulation and polymorphism.
3. Overloading vs. Overriding: Critical Differences
Overloading | Overriding |
---|---|
Same method name, different parameters (type, count, or order). | Same method signature in subclass. |
Resolved at compile time (static binding). | Resolved at runtime (dynamic binding). |
No inheritance required. | Requires inheritance. |
Example of Confusion:
class Animal {
void eat(String food) {
System.out.println("Animal eats " + food);
}
}
class Dog extends Animal {
// Intended to override but accidentally overloaded
void eat(int quantity) { // Different parameter type → overloading
System.out.println("Dog eats " + quantity + " times a day");
}
}
public class Main {
public static void main(String[] args) {
Animal a = new Dog();
a.eat("meat"); // Calls Animal's eat(String), not Dog's eat(int)
}
}
Why It Matters:
If you intend to override but accidentally overload (e.g., mismatched parameters), the superclass method will still be invoked, leading to unintended behavior.
Best Practice:
- Use the
@Override
annotation to enforce overriding:
@Override
void eat(String food) { ... } // Compiler error if signature mismatches
- Overloading: Use for methods with similar logic but different input types.
- Overriding: Use to customize subclass behavior.
Benefits of Polymorphism
- Code Flexibility: Write generalized code that works with multiple object types.
- Simplified Maintenance: Changes in subclass implementations don’t affect superclass code.
- Reusability: Promote clean, modular design through inheritance and interfaces.
Conclusion
Polymorphism is a cornerstone of Java’s OOP framework, enabling methods to adapt dynamically to the objects they interact with. By mastering method overloading (compile-time) and overriding (runtime), you can design flexible, scalable applications. Remember:
- Use method overloading for tasks requiring similar logic with different inputs.
- Use method overriding to customize subclass behavior.
- Leverage interfaces for multiple inheritance-like flexibility.
With practice, polymorphism becomes a powerful tool for writing elegant, maintainable Java code.
Related Reading: