Welcome to this lesson on Polymorphism! This is a core concept in Object-Oriented Programming (OOP) that allows you to write flexible, extensible, and reusable code. The word "polymorphism" comes from Greek, meaning "many forms." In programming, it refers to the ability of an object to take on many forms, or more precisely, the ability of a variable, function, or object to take on different types or to have different behaviors depending on the context.
Prerequisites: A Quick Recall
Before we dive deep into polymorphism, let's quickly recall two related concepts we've covered:
Inheritance: You know that inheritance allows a class (subclass/child class) to inherit properties and behaviors from another class (superclass/parent class). This creates an "is-a" relationship (e.g., a
Dogis aAnimal).Method Overriding: This is when a subclass provides its own specific implementation for a method that is already defined in its superclass. The method signature (name, return type, parameters) must be exactly the same.
Polymorphism builds directly on these concepts.
The Core Concept of Polymorphism: "Many Forms"
In Java, polymorphism primarily manifests in two ways:
Compile-time Polymorphism (Method Overloading):
This occurs when you have multiple methods in the same class with the same name but different parameters.
The compiler decides which method to call based on the arguments provided at compile time.
Example:
class Calculator { int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; } }When you call
calc.add(5, 10);, theintversion is chosen. When you callcalc.add(5.5, 10.1);, thedoubleversion is chosen. This is a simple form of polymorphism.
Runtime Polymorphism (Method Overriding / Dynamic Method Dispatch):
This is the more powerful and commonly referred to form of polymorphism.
It occurs when an object of a subclass is treated as an object of its superclass.
The method to be executed is determined at runtime by the Java Virtual Machine (JVM), based on the actual type of the object, not the type of the reference variable.
Let's focus on runtime polymorphism.
Key Principles of Runtime Polymorphism
1. Upcasting
When you assign an object of a subclass to a reference variable of its superclass type, this is called upcasting. It's always safe because a subclass object is a superclass object.
// Example: Animal is the superclass, Dog is the subclass
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override // Good practice to use @Override annotation
void makeSound() {
System.out.println("Dog barks!");
}
void fetch() {
System.out.println("Dog fetches the ball.");
}
}
public class PolymorphismDemo {
public static void main(String[] args) {
Animal myAnimal = new Dog(); // This is upcasting!
// The reference type is Animal, but the actual object type is Dog.
myAnimal.makeSound(); // Which makeSound() will be called?
// myAnimal.fetch(); // ERROR! Animal reference doesn't know about fetch()
}
}
Output:
Dog barks!
2. Dynamic Method Dispatch
In the example above, even though myAnimal is declared as an Animal type, when myAnimal.makeSound() is called, the makeSound() method from the Dog class is executed. This is because the JVM looks at the actual object (which is a Dog object) at runtime and invokes its overridden method. This process is called Dynamic Method Dispatch.
Practical Example: Shapes
Let's illustrate runtime polymorphism with a more common example involving shapes.
// 1. Superclass: Shape
// We make Shape an abstract class because it doesn't make sense to "draw" a generic Shape.
// It forces subclasses to implement the draw method.
abstract class Shape {
String name;
public Shape(String name) {
this.name = name;
}
// Abstract method - must be implemented by concrete subclasses
abstract void draw();
void displayName() {
System.out.println("This is a " + name);
}
}
// 2. Subclass: Circle
class Circle extends Shape {
double radius;
public Circle(String name, double radius) {
super(name); // Call superclass constructor
this.radius = radius;
}
@Override
void draw() {
System.out.println("Drawing a Circle with radius " + radius);
}
double calculateArea() {
return Math.PI * radius * radius;
}
}
// 3. Subclass: Rectangle
class Rectangle extends Shape {
double length;
double width;
public Rectangle(String name, double length, double width) {
super(name);
this.length = length;
this.width = width;
}
@Override
void draw() {
System.out.println("Drawing a Rectangle with length " + length + " and width " + width);
}
double calculateArea() {
return length * width;
}
}
// 4. Main class to demonstrate polymorphism
public class PolymorphicShapeDemo {
public static void main(String[] args) {
// Create an array of Shape references
Shape[] shapes = new Shape[3];
// Assign different actual object types to the Shape references
shapes[0] = new Circle("My Circle", 5.0); // Upcasting Circle to Shape
shapes[1] = new Rectangle("My Rectangle", 10.0, 4.0); // Upcasting Rectangle to Shape
shapes[2] = new Circle("Another Circle", 7.5); // Another Circle
// Iterate through the array and call the draw() method
// Notice we are calling draw() on a Shape reference,
// but the specific draw() method of the actual object is invoked at runtime.
for (Shape s : shapes) {
s.displayName(); // Common method inherited from Shape
s.draw(); // Polymorphic method call
System.out.println("---");
}
// Example of accessing subclass-specific method (requires downcasting, use with caution)
// If you need to access Circle's calculateArea method, you must downcast.
// This is generally discouraged if you can avoid it through better design.
if (shapes[0] instanceof Circle) { // Always check type before downcasting!
Circle c = (Circle) shapes[0]; // Downcasting from Shape to Circle
System.out.println("Area of " + c.name + ": " + c.calculateArea());
}
}
}
Output:
This is a My Circle
Drawing a Circle with radius 5.0
---
This is a My Rectangle
Drawing a Rectangle with length 10.0 and width 4.0
---
This is a Another Circle
Drawing a Circle with radius 7.5
---
Area of My Circle: 78.53981633974483
In this example, the Shape[] shapes array holds references to Shape objects, but the actual objects stored are Circle and Rectangle. When s.draw() is called inside the loop, Java correctly identifies whether s is a Circle or a Rectangle object at runtime and calls the appropriate draw() method. This is the power of runtime polymorphism!
Benefits of Polymorphism
Flexibility and Reusability: You can write code that works with a general superclass type, and that code will automatically work with any subclass that extends the superclass and overrides its methods. This means you don't have to write specific code for each new subclass.
Code Extensibility: Adding new subclasses (e.g., a
Triangleclass extendingShape) requires minimal changes to existing code (e.g., thePolymorphicShapeDemoloop remains unchanged). You just need to create the new class and ensure it implements the necessary methods.Simplified Code: Polymorphism reduces the need for
if-else ifstatements orswitchcases to determine an object's type and perform specific actions. Instead, you just call the polymorphic method, and the correct implementation is chosen for you.
Limitations and Considerations
Accessing Subclass-Specific Methods: When you use a superclass reference, you can only call methods defined in the superclass (or inherited from it). You cannot directly call methods that are unique to the subclass. To do so, you would need to downcast the object to its actual subclass type, but this should be done cautiously, usually with an
instanceofcheck to preventClassCastException.Performance (Minor): Dynamic method dispatch involves a slight overhead at runtime as the JVM determines which method to call. However, for most applications, this overhead is negligible.
Conclusion
Polymorphism is a cornerstone of Object-Oriented Programming in Java. It allows for highly flexible and maintainable code by enabling objects to be treated as instances of their superclasses, while still allowing their specific behaviors to be invoked at runtime. Understanding and utilizing polymorphism will significantly improve your ability to design robust and scalable Java applications.
Keep practicing with examples, and you'll soon master this powerful concept!
Key Takeaways
-
In programming, it's the ability of an object to take on many forms or for a method to behave differently based on the object it's called on.
-
Compile-time Polymorphism (Method Overloading): Methods with the same name but different parameters in the same class. Decided by the compiler.
Runtime Polymorphism (Method Overriding / Dynamic Method Dispatch): The more powerful and common form. An object of a subclass is treated as an object of its superclass. The actual method invoked is determined at runtime based on the actual object's type, not the reference type.
-
Flexibility & Reusability: Write generic code that works with different specific object types.
Extensibility: Easily add new subclasses without modifying existing code that uses the superclass.
Simplified Code: Reduces complex if-else if structures by letting the JVM handle method selection.