Java tutorials > Core Java Fundamentals > Object-Oriented Programming (OOP) > What is polymorphism and how is it achieved?
What is polymorphism and how is it achieved?
Polymorphism, derived from the Greek words 'poly' (many) and 'morph' (form), is one of the fundamental concepts of object-oriented programming (OOP). It refers to the ability of an object to take on many forms. More specifically, it allows you to treat objects of different classes in a uniform way. In Java, polymorphism is achieved through method overloading and method overriding.
Understanding Polymorphism
Polymorphism allows you to write code that can work with objects of different classes without knowing their specific types at compile time. This makes your code more flexible, reusable, and maintainable. Java supports two main types of polymorphism: compile-time polymorphism (method overloading) and runtime polymorphism (method overriding).
Compile-Time Polymorphism (Method Overloading)
Method overloading occurs when a class has multiple methods with the same name but different parameter lists (different number of parameters, different types of parameters, or different order of parameters). The compiler determines which method to call based on the arguments passed to the method during compile time. This is also known as static polymorphism or early binding. In the example above, the Calculator
class has three add
methods. The compiler chooses the appropriate add
method based on the number and types of arguments provided.
class Calculator {
public int add(int a, int b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
public double add(double a, double b) {
return a + b;
}
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.add(2, 3)); // Output: 5
System.out.println(calc.add(2, 3, 4)); // Output: 9
System.out.println(calc.add(2.5, 3.5)); // Output: 6.0
}
}
Runtime Polymorphism (Method Overriding)
Method overriding occurs when a subclass provides a specific implementation for a method that is already defined in its superclass. The method in the subclass must have the same name, return type, and parameter list as the method in the superclass. The In the example above, the @Override
annotation is used to indicate that a method is intended to override a method in the superclass. The actual method to be executed is determined at runtime based on the object's actual type. This is also known as dynamic polymorphism or late binding.Dog
and Cat
classes override the makeSound
method of the Animal
class. When makeSound
is called on a Dog
object, the Dog
's version of the method is executed, and similarly for the Cat
object.
class Animal {
public void makeSound() {
System.out.println("Generic animal sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
}
public class Main {
public static void main(String[] args) {
Animal animal = new Animal();
Animal dog = new Dog();
Animal cat = new Cat();
animal.makeSound(); // Output: Generic animal sound
dog.makeSound(); // Output: Woof!
cat.makeSound(); // Output: Meow!
Animal[] animals = new Animal[3];
animals[0] = new Animal();
animals[1] = new Dog();
animals[2] = new Cat();
for (Animal a : animals) {
a.makeSound();
}
}
}
Concepts Behind the Snippets
The core concept behind polymorphism is the ability to treat different objects in a uniform manner through a common interface (e.g., a superclass). This is achieved through inheritance and interfaces. Inheritance enables subclasses to inherit behavior and state from superclasses, while interfaces define contracts that classes can implement. Polymorphism relies on the Liskov Substitution Principle, which states that subtypes should be substitutable for their base types without altering the correctness of the program.
Real-Life Use Case
Consider a drawing application that can draw different shapes (e.g., circles, rectangles, triangles). You can define an abstract class or interface called Shape
with a draw()
method. Each shape class (Circle
, Rectangle
, Triangle
) would then implement the Shape
interface and provide its own implementation of the draw()
method. The application can then store all shapes in a list of Shape
objects and iterate through the list, calling the draw()
method on each object. The appropriate draw()
method will be called based on the actual type of the object, demonstrating polymorphism.
Best Practices
Interview Tip
When discussing polymorphism in an interview, be sure to explain both compile-time (method overloading) and runtime (method overriding) polymorphism. Provide clear examples of each and explain how they contribute to code flexibility and reusability. You can also discuss the benefits of using interfaces and abstract classes to achieve polymorphism.
When to Use Polymorphism
Use polymorphism when you want to create code that can work with objects of different classes in a uniform way. It's especially useful when you have a hierarchy of classes with common behavior but different implementations. Polymorphism promotes code reuse and reduces code duplication.
Memory Footprint
Polymorphism itself does not significantly increase the memory footprint. However, using inheritance can increase the memory footprint of objects because subclasses inherit the fields of their superclasses. The v-table (virtual method table) used for dynamic dispatch in runtime polymorphism also adds a small overhead. However, the benefits of polymorphism in terms of code organization and maintainability usually outweigh the slight increase in memory consumption.
Alternatives
While polymorphism is a powerful tool, there are alternatives in some situations. Function pointers (in languages like C++) can be used to achieve similar results, but they are less type-safe. Conditional statements (e.g., switch statements) can also be used, but this approach can lead to less maintainable and less extensible code compared to polymorphism.
Pros
Cons
FAQ
-
What is the difference between method overloading and method overriding?
Method overloading occurs within a single class when you have multiple methods with the same name but different parameter lists. Method overriding occurs when a subclass provides a specific implementation for a method that is already defined in its superclass.
-
What is the Liskov Substitution Principle?
The Liskov Substitution Principle states that subtypes should be substitutable for their base types without altering the correctness of the program. This is a key principle for ensuring that polymorphism works correctly.
-
Why is polymorphism important in object-oriented programming?
Polymorphism is important because it allows you to write code that can work with objects of different classes in a uniform way. This makes your code more flexible, reusable, and maintainable.