Java Polymorphism: A Comprehensive Guide with Examples and Exercises
Polymorphism is a core concept of object-oriented programming (OOP) that allows methods or functions to behave differently based on the object that calls them. In Java, polymorphism enables you to define one interface or method and have multiple implementations for it. This guide will cover the basics of polymorphism in Java, its types, and practical examples with exercises.
Table of Contents
- What is Polymorphism?
- Types of Polymorphism in Java
- Why Use Polymorphism?
- Method Overloading (Compile-Time Polymorphism)
- Method Overriding (Runtime Polymorphism)
- Polymorphism with Interfaces and Abstract Classes
- Exercises
1. What is Polymorphism?
Polymorphism is derived from two Greek words: poly (many) and morph (forms). In Java, polymorphism allows methods or objects to process differently depending on their data types or classes. The main advantage is that it enables you to perform a single action in different ways, thus making your code more flexible and easier to manage.
In simpler terms:
- Compile-time Polymorphism is achieved through method overloading.
- Runtime Polymorphism is achieved through method overriding.
2. Types of Polymorphism in Java
There are two types of polymorphism in Java:
- Compile-Time Polymorphism (also known as Static Polymorphism)
- Runtime Polymorphism (also known as Dynamic Polymorphism)
Let’s look at each type in detail.
3. Why Use Polymorphism?
Polymorphism makes code more modular, readable, and easy to maintain. It lets us define a single interface that can have multiple implementations, thereby simplifying complex hierarchies in object-oriented design. Polymorphism also facilitates code reusability and flexibility, which can be crucial for large applications.
4. Method Overloading (Compile-Time Polymorphism)
Method Overloading occurs when multiple methods in the same class have the same name but different parameter lists (type, number, or order). The Java compiler determines which method to call based on the method signature.
Example of Method Overloading
class Adder {
public int add(int a, int b) {
return a + b;
}
public double add(double a, double b) {
return a + b;
}
public int add(int a, int b, int c) {
return a + b + c;
}
}
public class Main {
public static void main(String[] args) {
Adder adder = new Adder();
System.out.println(adder.add(5, 10)); // Calls add(int, int)
System.out.println(adder.add(5.5, 3.2)); // Calls add(double, double)
System.out.println(adder.add(1, 2, 3)); // Calls add(int, int, int)
}
}
Explanation
In the Adder
class, the add
method is overloaded to accept different parameters. Based on the arguments passed, the compiler selects the appropriate method during compile-time.
5. Method Overriding (Runtime Polymorphism)
Method Overriding occurs when a subclass provides a specific implementation of a method already defined in its superclass. Here, Java uses dynamic binding, where the method to call is determined at runtime based on the object type.
Example of Method Overriding
class Animal {
public void sound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void sound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
public void sound() {
System.out.println("Cat meows");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog(); // Upcasting
Animal myCat = new Cat(); // Upcasting
myDog.sound(); // Outputs "Dog barks"
myCat.sound(); // Outputs "Cat meows"
}
}
Explanation
In this example, Dog
and Cat
override the sound
method of the Animal
superclass. At runtime, the Java Virtual Machine (JVM) determines which version of the sound
method to call, based on the object type (i.e., Dog
or Cat
).
6. Polymorphism with Interfaces and Abstract Classes
Polymorphism is also applied using interfaces and abstract classes. Since interfaces and abstract classes can define methods without implementing them, subclasses or implementing classes can define these methods according to their specific needs.
Example with Interface
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() {
System.out.println("Drawing a Circle");
}
}
class Rectangle implements Shape {
public void draw() {
System.out.println("Drawing a Rectangle");
}
}
public class Main {
public static void main(String[] args) {
Shape circle = new Circle();
Shape rectangle = new Rectangle();
circle.draw(); // Outputs "Drawing a Circle"
rectangle.draw(); // Outputs "Drawing a Rectangle"
}
}
Explanation
In this example, Circle
and Rectangle
implement the Shape
interface and provide specific implementations of the draw
method. This allows polymorphic behavior by treating circle
and rectangle
as Shape
objects, even though they are actually Circle
and Rectangle
instances.
7. Exercises
Exercise 1: Compile-Time Polymorphism (Method Overloading)
Create a class Calculator
with overloaded methods multiply
that:
- Accepts two integer values and returns their product.
- Accepts two double values and returns their product.
- Accepts three integer values and returns their product.
Write a main
method to test each multiply
method.
Exercise 2: Runtime Polymorphism (Method Overriding)
Create a superclass Vehicle
with a method move()
, which prints a generic message. Then, create two subclasses Car
and Bike
that override move()
to print messages specific to each vehicle type.
Write a main
method where:
- You create an array of
Vehicle
references containing instances ofCar
andBike
. - Loop through the array and call the
move()
method on eachVehicle
to see polymorphism in action.
Exercise 3: Polymorphism with Interfaces
Define an interface Playable
with a method play()
. Create two classes Piano
and Guitar
that implement Playable
and provide specific implementations for the play
method.
In your main
method:
- Create instances of
Piano
andGuitar
. - Store them in an array of
Playable
references and call theplay()
method on each element in the array.
Sample Solution for Exercise 1
class Calculator {
public int multiply(int a, int b) {
return a * b;
}
public double multiply(double a, double b) {
return a * b;
}
public int multiply(int a, int b, int c) {
return a * b * c;
}
}
public class Main {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println(calc.multiply(4, 5)); // Uses int multiply(int, int)
System.out.println(calc.multiply(2.5, 3.5)); // Uses double multiply(double, double)
System.out.println(calc.multiply(2, 3, 4)); // Uses int multiply(int, int, int)
}
}
Conclusion
Polymorphism is a powerful concept in Java that brings flexibility and scalability to your code. By mastering polymorphism, you can create more adaptable and reusable code structures. With method overloading and overriding, you have two primary ways to achieve polymorphism. Using exercises, you can deepen your understanding and ability to apply polymorphism effectively. Happy coding!