Polymorphism in Java
Polymorphism allows methods to perform differently based on the object that invokes them. It enhances flexibility and maintainability in code. Polymorphism can be classified into two categories:
-
Compile-time Polymorphism (also known as Method Overloading): Achieved by defining multiple methods in the same class with the same name but different parameter lists.
-
Runtime Polymorphism (also known as Method Overriding): Occurs when a subclass provides a specific implementation of a method that is already defined in its superclass. The method to be executed is determined at runtime.
Example 1: Method Overloading
Concept: Method overloading is when multiple methods have the same name but different parameter types or counts.
class Calculator {
// Method overloading: same method name, different parameters
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 PolymorphismExample1 {
public static void main(String[] args) {
Calculator calc = new Calculator();
System.out.println("Sum (int): " + calc.add(5, 10)); // Calls add(int, int)
System.out.println("Sum (double): " + calc.add(5.5, 10.5)); // Calls add(double, double)
System.out.println("Sum (3 ints): " + calc.add(1, 2, 3)); // Calls add(int, int, int)
}
}
Explanation: The Calculator
class defines multiple add
methods. Depending on the argument types or count, the appropriate method is invoked. This is determined at compile time.
Example 2: Method Overriding
Concept: Method overriding allows a subclass to provide a specific implementation of a method that is already defined in its superclass.
class Animal {
void sound() {
System.out.println("Animals make different sounds");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Cat meows");
}
}
public class PolymorphismExample2 {
public static void main(String[] args) {
Animal a = new Dog(); // Upcasting
a.sound(); // Dog's sound() method is called
a = new Cat(); // Reassigning to Cat object
a.sound(); // Cat's sound() method is called
}
}
Explanation: The Animal
class has a sound
method that is overridden by Dog
and Cat
. The method invoked depends on the actual object type (Dog
or Cat
), demonstrating runtime polymorphism.
Example 3: Constructor Overloading
Concept: Similar to method overloading, constructor overloading allows multiple constructors with different parameters within the same class.
class Person {
private String name;
private int age;
// Overloaded constructors
public Person() {
this.name = "Unknown";
this.age = 0;
}
public Person(String name) {
this.name = name;
this.age = 0;
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void display() {
System.out.println("Name: " + name + ", Age: " + age);
}
}
public class PolymorphismExample3 {
public static void main(String[] args) {
Person p1 = new Person();
Person p2 = new Person("John");
Person p3 = new Person("Jane", 30);
p1.display(); // Output: Name: Unknown, Age: 0
p2.display(); // Output: Name: John, Age: 0
p3.display(); // Output: Name: Jane, Age: 30
}
}
Explanation: The Person
class has three constructors, allowing for flexibility when creating objects. Different constructors are called based on the parameters provided.
Example 4: Runtime Polymorphism with Interfaces
Concept: Interfaces allow different classes to implement the same set of methods, enabling polymorphic behavior.
interface Vehicle {
void drive();
}
class Car implements Vehicle {
public void drive() {
System.out.println("Car is driving");
}
}
class Bike implements Vehicle {
public void drive() {
System.out.println("Bike is driving");
}
}
public class PolymorphismExample4 {
public static void main(String[] args) {
Vehicle v = new Car();
v.drive(); // Car's drive() is called
v = new Bike();
v.drive(); // Bike's drive() is called
}
}
Explanation: The Vehicle
interface is implemented by Car
and Bike
. The variable v
can reference any object that implements the Vehicle
interface, and the method called depends on the actual object type.
Example 5: Overloading with Different Data Types
Concept: Method overloading can also be achieved by changing the parameter types.
class Print {
public void print(int i) {
System.out.println("Integer: " + i);
}
public void print(double d) {
System.out.println("Double: " + d);
}
public void print(String s) {
System.out.println("String: " + s);
}
}
public class PolymorphismExample5 {
public static void main(String[] args) {
Print print = new Print();
print.print(10); // Calls print(int)
print.print(10.5); // Calls print(double)
print.print("Hello"); // Calls print(String)
}
}
Explanation: The Print
class demonstrates overloading based on different data types. The appropriate print
method is called based on the argument type.
Example 6: Polymorphism with Abstract Classes
Concept: Abstract classes can have abstract methods, which must be implemented by subclasses.
abstract class Shape {
abstract void draw();
}
class Circle extends Shape {
void draw() {
System.out.println("Drawing a circle");
}
}
class Square extends Shape {
void draw() {
System.out.println("Drawing a square");
}
}
public class PolymorphismExample6 {
public static void main(String[] args) {
Shape shape = new Circle();
shape.draw(); // Circle's draw() method is called
shape = new Square();
shape.draw(); // Square's draw() method is called
}
}
Explanation: The Shape
abstract class defines an abstract method draw
. Each subclass (Circle
and Square
) provides its own implementation. The method called depends on the actual object type.
Example 7: Upcasting in Polymorphism
Concept: Upcasting refers to casting a subclass object to a superclass reference type.
class Animal {
void sound() {
System.out.println("Animal makes sound");
}
}
class Dog extends Animal {
void sound() {
System.out.println("Dog barks");
}
}
public class PolymorphismExample7 {
public static void main(String[] args) {
Animal animal = new Dog(); // Upcasting
animal.sound(); // Dog's sound() method is called
}
}
Explanation: Here, the Dog
object is referenced by an Animal
type variable. This upcasting allows us to invoke the overridden sound
method of the Dog
class.
Example 8: Polymorphism with Multiple Classes
Concept: Polymorphism allows one interface or abstract class to be implemented by multiple classes.
class Shape {
void draw() {
System.out.println("Drawing a shape");
}
}
class Circle extends Shape {
void draw() {
System.out.println("Drawing a circle");
}
}
class Triangle extends Shape {
void draw() {
System.out.println("Drawing a triangle");
}
}
public class PolymorphismExample8 {
public static void main(String[] args) {
Shape shape;
shape = new Circle();
shape.draw(); // Circle's draw() method is called
shape = new Triangle();
shape.draw(); // Triangle's draw() method is called
}
}
Explanation: The Shape
class is the base class, and both Circle
and Triangle
provide their own implementations of draw()
. The method invoked depends on the actual object assigned to shape
.
Example 9: Polymorphism with Collections
Concept: Collections can hold references to objects of different types, demonstrating polymorphic behavior.
import java.util.ArrayList;
import java.util.List;
class Animal {
void sound() {
System.out.println("Animal makes sound");
}
}
class Dog extends Animal {
void sound() {
System.out.println("Dog barks");
}
}
class Cat extends Animal {
void sound() {
System.out.println("Cat meows");
}
}
public class PolymorphismExample9 {
public static void main(String[] args) {
List<Animal> animals = new ArrayList
<>();
animals.add(new Dog());
animals.add(new Cat());
for (Animal animal : animals) {
animal.sound(); // Polymorphic behavior
}
}
}
Explanation: A list of Animal
references can contain Dog
and Cat
objects. The appropriate sound
method is called based on the actual object type, demonstrating polymorphism in collections.
Example 10: Overloading with Varargs
Concept: Varargs allows a method to accept variable-length arguments, enabling flexibility in method calls.
class Adder {
public int add(int... numbers) {
int sum = 0;
for (int num : numbers) {
sum += num;
}
return sum;
}
}
public class PolymorphismExample10 {
public static void main(String[] args) {
Adder adder = new Adder();
System.out.println("Sum: " + adder.add(1, 2, 3, 4, 5)); // Calls add(int...)
}
}
Explanation: The add
method accepts a variable number of integer arguments. This method can be called with any number of arguments, enhancing flexibility.
Example 11: Runtime Polymorphism with Superclass Reference
Concept: The superclass reference can hold the subclass object, allowing for dynamic method dispatch.
class Employee {
void work() {
System.out.println("Employee is working");
}
}
class Manager extends Employee {
void work() {
System.out.println("Manager is managing");
}
}
public class PolymorphismExample11 {
public static void main(String[] args) {
Employee emp = new Manager(); // Upcasting
emp.work(); // Manager's work() method is called
}
}
Explanation: The Employee
reference emp
holds a Manager
object. When work()
is called, the Manager
's overridden method is executed due to dynamic binding.
Example 12: Polymorphism with Final Methods
Concept: Methods declared as final
cannot be overridden, maintaining their behavior across subclasses.
class Vehicle {
final void run() {
System.out.println("Vehicle is running");
}
}
class Car extends Vehicle {
// run() cannot be overridden because it's final in Vehicle
}
public class PolymorphismExample12 {
public static void main(String[] args) {
Vehicle vehicle = new Car();
vehicle.run(); // Calls Vehicle's final run() method
}
}
Explanation: The run
method in Vehicle
is final, so it cannot be overridden by Car
. When run
is called, the original implementation from Vehicle
is executed.
Example 13: Covariant Return Type in Polymorphism
Concept: Covariant return types allow a method to return a subclass type instead of the superclass type.
class Animal {
Animal get() {
return this;
}
}
class Dog extends Animal {
@Override
Dog get() {
return this; // Returns Dog instance
}
}
public class PolymorphismExample13 {
public static void main(String[] args) {
Animal animal = new Dog();
Dog dog = (Dog) animal.get(); // Downcasting
System.out.println("Dog instance: " + dog);
}
}
Explanation: The get
method in Dog
returns a Dog
type instead of Animal
, demonstrating covariant return types. The animal
reference can call get
, returning a Dog
instance.
Example 14: Dynamic Method Dispatch
Concept: Java uses dynamic method dispatch to resolve method calls at runtime based on the object's actual type.
class A {
void show() {
System.out.println("Class A");
}
}
class B extends A {
void show() {
System.out.println("Class B");
}
}
public class PolymorphismExample14 {
public static void main(String[] args) {
A obj = new B(); // Upcasting
obj.show(); // Class B is printed due to dynamic method dispatch
}
}
Explanation: In this example, obj
is an A
type reference but holds a B
type object. The show
method executed is from class B
, showcasing dynamic method dispatch.
Example 15: Polymorphism with Getters and Setters
Concept: Getters and setters can be polymorphic by changing the return type.
class Employee {
String getDetails() {
return "Employee details";
}
}
class Manager extends Employee {
@Override
String getDetails() {
return "Manager details";
}
}
public class PolymorphismExample15 {
public static void main(String[] args) {
Employee emp = new Manager(); // Upcasting
System.out.println(emp.getDetails()); // Calls Manager's getDetails()
}
}
Explanation: The getDetails
method in Employee
is overridden in Manager
. When getDetails
is called on emp
, the Manager
version is executed, illustrating polymorphic behavior.
Example 16: Polymorphism with Static Methods
Concept: Static methods cannot be overridden; instead, they are hidden.
class Parent {
static void display() {
System.out.println("Parent's display method");
}
}
class Child extends Parent {
static void display() {
System.out.println("Child's display method");
}
}
public class PolymorphismExample16 {
public static void main(String[] args) {
Parent obj = new Child();
obj.display(); // Calls Parent's display() method due to static method hiding
}
}
Explanation: In this example, display
is a static method. When called on a Parent
reference pointing to a Child
object, it calls the Parent
's version, showcasing static method hiding.
Example 17: Polymorphism in Event Handling
Concept: Polymorphism is commonly used in event handling, where a single event can have multiple handlers.
import java.awt.*;
import java.awt.event.*;
public class PolymorphismExample17 {
public static void main(String[] args) {
Frame f = new Frame("Polymorphism Example");
Button b = new Button("Click Me");
// Using polymorphism in event handling
b.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Button clicked!");
}
});
f.add(b);
f.setSize(300, 300);
f.setLayout(new FlowLayout());
f.setVisible(true);
}
}
Explanation: The ActionListener
interface is implemented by an anonymous class. The button's click event can be handled polymorphically, allowing for different actions to occur based on different event sources.
Example 18: Polymorphism with Lambda Expressions
Concept: Lambda expressions enable polymorphic behavior in functional interfaces.
import java.util.function.Function;
public class PolymorphismExample18 {
public static void main(String[] args) {
Function<Integer, Integer> square = x -> x * x;
Function<Integer, Integer> cube = x -> x * x * x;
System.out.println("Square: " + square.apply(5)); // Output: Square: 25
System.out.println("Cube: " + cube.apply(3)); // Output: Cube: 27
}
}
Explanation: Here, two lambda expressions are assigned to a Function
interface. This allows different behavior (squaring and cubing) to be executed based on which lambda expression is called.
Example 19: Polymorphism in GUI Applications
Concept: In GUI applications, polymorphism allows components to behave differently based on their types.
import javax.swing.*;
import java.awt.event.*;
public class PolymorphismExample19 {
public static void main(String[] args) {
JFrame frame = new JFrame("Polymorphism Example");
JButton button = new JButton("Press Me");
JTextField textField = new JTextField(20);
// Using polymorphism for event handling
button.addActionListener(e -> textField.setText("Button Pressed!"));
frame.add(button);
frame.add(textField);
frame.setSize(300, 300);
frame.setLayout(new FlowLayout());
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}
Explanation: This example demonstrates using polymorphism in GUI components. The button click modifies the text field, showing how different components can respond to events polymorphically.
Example 20: Polymorphism in Data Processing
Concept: Data processing classes can use polymorphism to handle various data formats.
abstract class DataProcessor {
abstract void process();
}
class CSVProcessor extends DataProcessor {
void process() {
System.out.println("Processing CSV data");
}
}
class JSONProcessor extends DataProcessor {
void process() {
System.out.println("Processing JSON data");
}
}
public class PolymorphismExample20 {
public static void main(String[] args) {
DataProcessor processor;
processor = new CSVProcessor();
processor.process(); // Processing CSV data
processor = new JSONProcessor();
processor.process(); // Processing JSON data
}
}
Explanation: The DataProcessor
abstract class defines the process
method. CSVProcessor
and JSONProcessor
provide specific implementations, allowing
different data formats to be processed polymorphically.