In Java, polymorphism is a fundamental concept in object-oriented programming that enables a single method name to execute various actions depending on the object or input parameters. This section provides a comprehensive overview of polymorphism, including its uses and demonstrations.
What is Polymorphism in Java?
Polymorphism is a fundamental principle in object-oriented programming that empowers a single method or action to exhibit varied behaviors. This feature enables a method with the same name to execute diverse operations based on the calling object or the input parameters it receives.
The term polymorphism originates from two Greek terms: poly and morphs. Specifically, poly signifies numerous, while morphs refers to shapes. Hence, polymorphism denotes the existence of numerous forms.
In Java, polymorphism manifests in two forms:
- Compile-time Polymorphism
- Runtime Polymorphism.
Polymorphism in Java can be achieved through method overloading and method overriding techniques.
1. Java Compile-Time Polymorphism
In Java, method overloading enables achieving compile-time polymorphism. By employing method overloading, a class can contain numerous methods sharing identical names but varying parameter lists. The compiler identifies the appropriate method to execute during compilation by evaluating the count, type, and sequence of parameters provided.
When employing method overloading, it is essential for methods to share identical names while varying in either the quantity or data types of parameters they accept. During method invocation, the compiler identifies the most appropriate overloaded method by considering the supplied arguments. If a precise match is identified, that specific method is executed; alternatively, the compiler utilizes type promotion to determine the most closely matching method.
Example of Compile-Time Polymorphism
The subsequent illustration showcases how compiler-time polymorphism is achieved through method overloading:
Example
class Calculation {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
}
public class Main {
public static void main(String[] args) {
Calculation calc = new Calculation();
// Compile-time polymorphism: selecting the appropriate add method based on parameter types
System.out.println("Sum of integers: " + calc.add(5, 3));
System.out.println("Sum of doubles: " + calc.add(2.5, 3.7));
}
}
Output:
Sum of integers: 8
Sum of doubles: 6.2
Explanation
In this instance, the Calculation class showcases two add functions: one designed for receiving two integers and the other tailored for processing two doubles. The compiler determines which add method to invoke based on the data types of the arguments passed in the method call. This decision occurs at compile time, demonstrating the concept of polymorphism.
2. Runtime Polymorphism in Java
Dynamic Method Dispatch, also referred to as runtime polymorphism, is the mechanism where the invocation of a method that has been overridden is determined during runtime instead of during compile time.
During this procedure, a superclass reference variable pointing to a subclass object is used to call an overridden method. The method that is executed is based on the actual object being referenced, rather than the type of reference.
Before delving into the concept of Runtime Polymorphism, it is essential to grasp the concept of upcasting.
Upcasting
Upcasting occurs when a reference variable of a parent class is used to refer to an object of a child class. An instance of this is:
class A{}
class B extends A{}
A a=new B();//upcasting
When performing upcasting, it is possible to utilize the reference variable of either a class type or an interface type. An illustration is shown below:
interface I{}
class A{}
class B extends A implements I{}
Here, the relationship of B class would be:
B IS-A A
B IS-A I
B IS-A Object
Given that in Java, the class Object serves as the superclass for all other classes, it is valid to state that class B is a subtype of Object.
Example of Runtime Polymorphism
In the given illustration, we are defining two classes named Bicycle and Superbike. The Superbike class is a subclass of the Bicycle class and it redefines the run method from its parent class. The run method is invoked using the reference variable of the parent class. As this reference pertains to the subclass object and the subclass method overrides the method in the parent class, the subclass method is executed dynamically at runtime.
Runtime polymorphism is a concept where method invocation is decided by the JVM instead of the compiler.
Example
class Bike{
void run(){System.out.println("running");}
}
class Splendor extends Bike{
void run(){System.out.println("running safely with 60km");}
}
public class Main{
public static void main(String args[]){
Bike b = new Splendor();//upcasting
b.run();
}
}
Output:
running safely with 60km.
Explanation
In this Java demonstration, method overriding is utilized to showcase runtime polymorphism. The Splendour class extends the Bike class and overrides the run method to display the message "running safely with 60km," while the Bike class contains a run method that outputs "running." Through upcasting, a reference variable named b of type Bike is created in the main method, which references an instance of Splendour. The runtime polymorphism is exemplified by the overridden run method in the Splendour class, which is invoked when the run method is called using this reference variable. The Java Virtual Machine (JVM) determines the appropriate method to execute based on the actual object type during runtime.
Real Life Examples of Runtime Polymorphism
Explore the practical instances of runtime polymorphism through the following real-world examples to enhance your comprehension of the concept:
Example 1: Bank Class
Imagine a situation where there is a class called Bank that contains a function for retrieving the interest rate. It's important to note that the interest rate can vary depending on the bank. For instance, SBI, ICICI, and AXIS banks offer interest rates of 8.4%, 7.3%, and 9.7% respectively.
Note: This example is also given in method overriding but there was no upcasting.
Source Code
class Bank{
float getRateOfInterest(){return 0;}
}
class SBI extends Bank{
float getRateOfInterest(){return 8.4f;}
}
class ICICI extends Bank{
float getRateOfInterest(){return 7.3f;}
}
class AXIS extends Bank{
float getRateOfInterest(){return 9.7f;}
}
public class Main{
public static void main(String args[]){
Bank b;
b=new SBI();
System.out.println("SBI Rate of Interest: "+b.getRateOfInterest());
b=new ICICI();
System.out.println("ICICI Rate of Interest: "+b.getRateOfInterest());
b=new AXIS();
System.out.println("AXIS Rate of Interest: "+b.getRateOfInterest());
}
}
Output:
SBI Rate of Interest: 8.4
ICICI Rate of Interest: 7.3
AXIS Rate of Interest: 9.7
Explanation
In this Java example, method overriding is utilized to showcase polymorphism. It showcases a getRateOfInterest method within the Bank class, which initially returns a value of 0. Subsequently, subclasses such as SBI, ICICI, and AXIS extend the Bank class and override the getRateOfInterest method to provide specific interest rates. Within the TestPolymorphism class's main method, instances of SBI, ICICI, and AXIS are created and linked to a Bank reference variable, denoted as 'b'. Through polymorphism, the appropriate getRateOfInterest method is dynamically invoked based on the actual object type assigned to variable 'b', leading to the display of interest rates specific to SBI, ICICI, and AXIS banks.
Example 2: Shape Class
Let's consider a different instance to illustrate the shape class with the utilization of runtime polymorphism.
Source Code
class Shape{
void draw(){System.out.println("drawing...");}
}
class Rectangle extends Shape{
void draw(){System.out.println("drawing rectangle...");}
}
class Circle extends Shape{
void draw(){System.out.println("drawing circle...");}
}
class Triangle extends Shape{
void draw(){System.out.println("drawing triangle...");}
}
public class Main{
public static void main(String args[]){
Shape s;
s=new Rectangle();
s.draw();
s=new Circle();
s.draw();
s=new Triangle();
s.draw();
}
}
Output:
drawing rectangle...
drawing circle...
drawing triangle...
Explanation
Within the realm of geometric figures, the following Java script illustrates the concept of polymorphism by means of method overriding. It features a draw function that displays "drawing..." within a foundational Shape class. Shape is extended by the subclasses Rectangle, Circle, and Triangle, each of which offers unique implementations for their respective shapes by overriding the draw method. In the main method of the TestPolymorphism2 class, instances of Shape reference variables, denoted as s, are instantiated and linked to Rectangle, Circle, and Triangle objects. Through polymorphism, the draw function dynamically invokes the appropriate implementation based on the actual object type assigned to variable s. Consequently, the program generates the outputs "drawing triangle...", "drawing circle...", and "drawing rectangle..." for each shape, showcasing how Java's polymorphism feature enables adaptability and dynamic behavior.
Example 3: Animal Class
Let's explore another example to illustrate the concept of runtime polymorphism by utilizing the Animal Class.
Source Code
class Animal{
void eat(){System.out.println("eating...");}
}
class Dog extends Animal{
void eat(){System.out.println("eating bread...");}
}
class Cat extends Animal{
void eat(){System.out.println("eating rat...");}
}
class Lion extends Animal{
void eat(){System.out.println("eating meat...");}
}
public class Main{
public static void main(String[] args){
Animal a;
a=new Dog();
a.eat();
a=new Cat();
a.eat();
a=new Lion();
a.eat();
}
}
Output:
eating bread...
eating rat...
eating meat...
Explanation
In this Java example, method overriding is utilized to showcase polymorphism. The scenario involves the creation of an eat method within the base class Animal, which is then redefined by the derived classes Dog, Cat, and Lion to exhibit distinct eating behaviors. Polymorphic behavior is exemplified by assigning instances of different subclasses to an Animal reference variable 'an' within the main function. Subsequently, this reference is employed to invoke the eat function, which dynamically triggers the corresponding override implementation according to the specific object type.
Java Runtime Polymorphism with Data Member
Runtime polymorphism cannot be achieved by data members because it is the method that is overridden, not the data members themselves.
In the provided scenario, both classes contain a variable named speedlimit. The data member is accessed using the reference variable of the Parent class that points to the subclass object. As the data member is not overridden, it will consistently access the data member of the Parent class.
Rule: Runtime polymorphism can't be achieved by data members.
Example
class Bike{
int speedlimit=90;
}
class Honda extends Bike{
int speedlimit=150;
}
public class Main{
public static void main(String args[]){
Bike obj=new Honda();
System.out.println(obj.speedlimit);//90
}
}
Output:
Explanation
Within this Java code, there exists a class hierarchy comprising the base class Bike and its subclass Honda3. The Bike class has a member variable named speedlimit, which is set to 90 upon initialization. In contrast, the Honda3 class initializes its speedlimit variable to 150. Despite the creation of a Bike-type object within the main method of the Honda3 class, due to upcasting, it is referenced by the Honda3 reference variable obj. Consequently, when obj.speedlimit is accessed and printed, the output is 90 instead of 150.
Java's member variables lack polymorphism, meaning that compile-time access to these variables depends on the reference type rather than the object's actual type. In this case, even though the object is a Honda3, the reference to the speedlimit member variable is determined by the Bike class.
Java Runtime Polymorphism with Multilevel Inheritance
Let's examine a straightforward illustration of Runtime Polymorphism involving multilevel inheritance.
Example
class Animal{
void eat(){System.out.println("eating");}
}
class Dog extends Animal{
void eat(){System.out.println("eating fruits");}
}
class BabyDog extends Dog{
void eat(){System.out.println("drinking milk");}
}
public class Main{
public static void main(String args[]){
Animal a1,a2,a3;
a1=new Animal();
a2=new Dog();
a3=new BabyDog();
a1.eat();
a2.eat();
a3.eat();
}
}
Output:
eating
eating fruits
drinking Milk
Explanation
The following Java example demonstrates polymorphism and method overriding within a hierarchy of classes related to animals. In this scenario, the classes Dog and BabyDog extend the Animal class. They both override the eat method, with Dog printing "eating fruits" and BabyDog printing "drinking milk." On the other hand, the Animal class has its own eat method that prints "eating."
The main method showcases polymorphic behavior by instantiating and invoking objects of Animal, Dog, and BabyDog classes using the eat method. This results in dynamically selecting the appropriate overridden implementation based on the actual type of object assigned to the reference variable.
Example
class Animal{
void eat(){System.out.println("animal is eating...");}
}
class Dog extends Animal{
void eat(){System.out.println("dog is eating...");}
}
class BabyDog extends Dog{}
public class Main{
public static void main(String args[]){
Animal a=new BabyDog();
a.eat();
}
}
Explanation
Demonstrating polymorphism and method overriding in Java, the code showcases a class hierarchy involving Animal as the base class, Dog extending Animal and overriding the eat method to display "dog is eating...", and BabyDog extending Dog. The Animal class also contains an eat method that outputs "animal is eating..." During the execution, an instance of BabyDog is created and assigned to an Animal reference variable in the main method. When invoking a.eat, the overridden eat function from the BabyDog class is dynamically executed, showcasing polymorphic behavior where the method invoked is based on the specific object type at runtime.
Advantages of Polymorphism
Polymorphism in Java offers numerous benefits. Here are a few of them:
- Enhanced code reusability
Polymorphism enables methods in derived classes to replace methods in their base class, promoting code reusability and ensuring a uniform interface among associated classes.
- Enhanced Flexibility and Expandability
Polymorphism enables subclasses to override methods from the superclass, offering a way to expand and tailor functionality without altering the original code.
- Dynamic Method Invocation:
Polymorphism allows for dynamic method invocation, meaning that the specific method being executed is based on the actual type of the object during runtime, offering versatility in method dispatch.
- Implementing Interfaces:
In Java, interfaces provide a way for multiple classes to implement the same interface with unique implementations. This feature supports polymorphic behavior, allowing objects from various classes to be handled interchangeably as long as they adhere to a shared interface.
- Function Overloading:
Polymorphism can also be accomplished by method overloading, a technique that involves defining multiple methods with identical names but distinct parameter lists in a class or its derived classes. This practice improves code readability and enables adaptable method invocation depending on parameter types.
- Decreased Code Complexity:
Polymorphism plays a key role in simplifying code complexity through the encouragement of a structured and layered class system, which enhances the comprehensibility, manageability, and scalability of extensive software systems.