What is Polymorphism?
A key idea in object-oriented programming (OOP) is polymorphism, which enables objects of many classes to be viewed as belonging to a single superclass. It allows a single interface (method or class) to represent multiple types or behaviors. In simpler terms, polymorphism allows objects to take on different forms or shapes.
There are two main types of polymorphism in Java:
- Compile-time Polymorphism (Static Polymorphism):
- Compile-time polymorphism is achieved through method overloading and operator overloading.
- Method overloading allows a class to have multiple methods with the same name but different parameter lists. The compiler determines which method to call based on the number or types of arguments passed during the method invocation.
- Operator overloading is not directly supported in Java, but it is available in some other programming languages, where the behavior of operators (e.g., +, -, *, /) can be defined for user-defined types.
Method overloading:
Method overloading is a form of polymorphism in Java where multiple methods in the same class have the same name but different parameters. It allows a class to define multiple methods with similar functionality, but with different input parameters or data types, enabling more flexibility and ease of use.
Example of method overloading:
public class MathOperations { // Method to add two integers public int add(int a, int b) { return a + b; } // Method to add three integers public int add(int a, int b, int c) { return a + b + c; } // Method to add two double values public double add(double a, double b) { return a + b; } // Method to concatenate two strings public String add(String str1, String str2) { return str1 + str2; } }
Explanation:
- In the MathOperations class, we have multiple methods named add, each with different parameter types.
- The first add method takes two integer parameters (int a and int b) and returns their sum.
- The second add method takes three integer parameters (int a, int b, and int c) and returns their sum.
- The third add method takes two double parameters (double a and double b) and returns their sum.
- The fourth add method takes two string parameters (String str1 and String str2) and returns their concatenation.
Now, let’s use the MathOperations class in the main method to demonstrate method overloading:
public class Main { public static void main(String[] args) { MathOperations math = new MathOperations(); int sum1 = math.add(2, 3); System.out.println("Sum of 2 and 3 is: " + sum1); // Output: "Sum of 2 and 3 is: 5" int sum2 = math.add(1, 2, 3); System.out.println("Sum of 1, 2, and 3 is: " + sum2); // Output: "Sum of 1, 2, and 3 is: 6" double sum3 = math.add(2.5, 3.5); System.out.println("Sum of 2.5 and 3.5 is: " + sum3); // Output: "Sum of 2.5 and 3.5 is: 6.0" String concatenatedStr = math.add("Hello, ", "Java!"); System.out.println("Concatenated string: " + concatenatedStr); // Output: "Concatenated string: Hello, Java!" } }
Explanation:
- In the main method, we create an object of the MathOperations class called math.
- We call the first add method with two integer arguments (2 and 3). The method with two integer parameters is invoked, and the result (5) is printed to the console.
- We call the second add method with three integer arguments (1, 2, and 3). The method with three integer parameters is invoked, and the result (6) is printed to the console.
- We call the third add method with two double arguments (5 and 3.5). The method with two double parameters is invoked, and the result (6.0) is printed to the console.
- Finally, we call the fourth add method with two string arguments (“Hello, “ and “Java!”). The method with two string parameters is invoked, and the concatenated string (“Hello, Java!”) is printed to the console.
In this example, method overloading allows us to use the same method name add to handle different types of inputs. The appropriate method is automatically selected based on the number and types of arguments passed to the method. This feature makes the code more intuitive, flexible, and readable, contributing to the principle of polymorphism, where a single method name can have different forms depending on the context or input.
- Run-time Polymorphism (Dynamic Polymorphism):
- Run-time polymorphism is achieved through 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 overrides the behavior of the method in the superclass for objects of the subclass.
- The determination of which method to call happens at runtime based on the actual type of the object, not the reference type.
- This is also known as dynamic binding, late binding, or virtual method invocation.
Method overriding:
Method overriding is another form of polymorphism in Java, where 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 method signature (name and parameters) as the method in the superclass.
Example of method overriding:
class Shape { void draw() { System.out.println("Drawing a shape"); } } class Circle extends Shape { @Override void draw() { System.out.println("Drawing a circle"); } } class Square extends Shape { @Override void draw() { System.out.println("Drawing a square"); } } public class Main { public static void main(String[] args) { Shape circle = new Circle(); Shape square = new Square(); circle.draw(); // Output: "Drawing a circle" square.draw(); // Output: "Drawing a square" } }
Explanation:
In this code, we have a superclass called Shape and two subclasses Circle and Square that extend the Shape class. Each of these subclasses overrides the draw() method from the Shape class with their own implementation.
Let’s walk through the code step by step:
- We have a superclass Shape with a method draw(), which prints “Drawing a shape” to the console.
- The Circle class extends Shape and overrides the draw() In the Circle class, the draw() method is redefined to print “Drawing a circle” to the console.
- The Square class also extends Shape and overrides the draw() The draw() method in the Square class is redefined to print “Drawing a square” to the console.
- Next, two objects are created: circle and square, both of type Shape. However, they are actually instances of Circle and Square respectively because of polymorphism.
- When you call draw(), it invokes the draw() method on the circle object. Since the circle object is an instance of Circle, it executes the overridden draw() method in the Circle class. Hence, the output is “Drawing a circle”.
- Similarly, when you call draw(), it invokes the draw() method on the square object. Since the square object is an instance of Square, it executes the overridden draw() method in the Square class. The output is “Drawing a square”.
This behavior is possible due to the concept of polymorphism, where an object of a subclass can be treated as an object of its superclass, and the appropriate method implementation is chosen dynamically at runtime based on the actual object’s type (not the reference type). In other words, the method called depends on the specific object’s class, not the type of the reference variable used to access the object.
Let’s provide another example of polymorphism using the concept of run-time polymorphism (method overriding).
Suppose we have a superclass called Animal, which defines a method named makeSound(). We also have two subclasses, Dog and Cat, both of which inherit from the Animal class and provide their own specific implementations of the makeSound() method.
class Animal { void makeSound() { System.out.println("Some generic animal sound"); } } class Dog extends Animal { @Override void makeSound() { System.out.println("Dog barks"); } } class Cat extends Animal { @Override void makeSound() { System.out.println("Cat meows"); } }
Now, let’s see how polymorphism works when we create objects of the Dog and Cat classes and call the makeSound() method:
public class Main { public static void main(String[] args) { Animal animal1 = new Dog(); Animal animal2 = new Cat(); animal1.makeSound(); // Output: "Dog barks" animal2.makeSound(); // Output: "Cat meows" } }
Explanation:
- We have a superclass Animal with a method makeSound(). The makeSound() method in the Animal class provides a generic implementation for an animal sound.
- The Dog and Cat classes are subclasses of Animal. Both Dog and Cat classes override the makeSound() method with their specific sound implementations.
- In the main method, we create two Animal references (animal1 and animal2) that are assigned objects of the Dog and Cat classes, respectively. This is allowed because of the “IS-A” relationship, where a Dog “IS-A” Animal, and a Cat “IS-A” Animal.
- During the method calls makeSound() and animal2.makeSound(), the actual method that gets executed depends on the actual type of the object that the reference points to, not the reference type. This is run-time polymorphism.
- At runtime, the JVM determines that animal1 points to a Dog object and animal2 points to a Cat Therefore, the respective overridden makeSound() methods in the Dog and Cat classes are invoked, resulting in the specific sound for each animal.
Polymorphism allows us to treat different objects with a common superclass reference, making the code more flexible and extensible. By using polymorphism, we can write code that can work with various subclasses without knowing their specific implementations, promoting code reusability and easier maintenance.
Example:
public class Account { String accName; int ssn; Account(String accName, int ssn) { this.accName=accName; this.ssn=ssn; } void calculateFee() { System.out.println("Account fee"); } }//class
public class Demo { public static void main(String[] args) { //Polymorphism //WebDriver driver = new ChromeDriver(); int age; //Primitive variable // Object variable declaration Account chkAcc; //here chkAcc is an object variable of Account Data Type chkAcc = new CheckingAccount(1, "karim", 3333); //but chkAcc is instantiated by CheckingAccount constructor //chkAcc is a CheckingAccount object of Account Data Type chkAcc.calculateFee(); Account savAcc = new SavingAccount(2, "Joe", 4444); savAcc.calculateFee(); Account acc = new Account("Maria", 7777); acc.calculateFee(); /* byte b=4; int i=3; i = b; b = (byte)i; acc = chkAcc; acc.calculateFee(); chkAcc = (Account)acc; chkAcc.calculateFee(); //acc chkAcc savAcc */ }//main }//class public class CheckingAccount extends Account{ int chkAccId; CheckingAccount(int chkAccId, String accName, int ssn){ super(accName, ssn); this.chkAccId=chkAccId; } @Override void calculateFee() { System.out.println("Checking Fee"); } }//class public class SavingAccount extends Account { int savAccId; SavingAccount(int savAccId, String accName, int ssn){ super(accName, ssn); this.savAccId=savAccId; } void calculateFee() { System.out.println("Saving Fee"); } }
we have a class hierarchy consisting of three classes: Account, CheckingAccount, and SavingAccount. Let’s go through each class and explain its purpose and behavior:
- Account Class:
- This is the base class, which represents a general account. It has two instance variables: accName (account name) and ssn (social security number).
- The constructor Account(String accName, int ssn) is used to initialize the accName and ssn
- The calculateFee() method in the Account class prints “Account fee” to the console.
- CheckingAccount Class:
- This is a subclass of Account, representing a specific type of account called a checking account.
- It has an additional instance variable chkAccId, which represents the checking account ID.
- The constructor CheckingAccount(int chkAccId, String accName, int ssn) is used to initialize the accName, ssn, and chkAccId variables by calling the superclass Account constructor using super(accName, ssn).
- The calculateFee() method in the CheckingAccount class overrides the calculateFee() method of the superclass. It prints “Checking Fee” to the console.
- SavingAccount Class:
- This is another subclass of Account, representing a specific type of account called a saving account. However, this class is not defined in the provided code snippet, so we’ll assume it’s defined elsewhere.
- Since the SavingAccount class is not shown, we cannot provide specific details about its implementation. However, it would typically have its own savingAccId instance variable and an overridden calculateFee() method specific to saving accounts.
- Demo Class:
- In the main method of the Demo class, we demonstrate polymorphism using the Account and its subclasses.
- We declare an object variable chkAcc of type Account and instantiate it with a CheckingAccount object using the constructor CheckingAccount(1, “karim”, 3333).
- Similarly, we create a SavingAccount object and store it in a savAcc variable of type Account.
- We also create an Account object and store it in an acc variable of the same type.
- We then call the calculateFee() method on each of these objects. Due to polymorphism and method overriding, the appropriate calculateFee() method is called based on the actual type of the object.
Explanation:
- The calculateFee() method is called on each object: chkAcc, savAcc, and acc.
- For chkAcc, which is a CheckingAccount object, the overridden calculateFee() method in the CheckingAccount class is executed, printing “Checking Fee” to the console.
- For savAcc, which is a SavingAccount object (not shown in the provided code), the overridden calculateFee() method specific to saving accounts would be executed if it were implemented.
- For acc, which is a basic Account object, the calculateFee() method of the Account class is executed, printing “Account fee” to the console.
In summary, polymorphism allows us to treat objects of different subclasses (CheckingAccount, SavingAccount) as objects of their common superclass (Account). This leads to more flexible and generic code, making it easier to work with related objects and promoting code reusability and maintainability.
Advantages of Polymorphism:
Polymorphism offers several advantages in object-oriented programming, contributing to code flexibility, reusability, and maintainability. Here are some key advantages of polymorphism:
- Flexibility and Extensibility: Polymorphism allows you to write code that can work with objects of different classes that share a common superclass or implement a common interface. This makes the code more flexible and extensible, as it can handle a variety of object types without requiring changes to the core implementation.
- Code Reusability: By using polymorphism, you can create generic algorithms and methods that operate on a common interface. These algorithms can be reused with different implementations, promoting code reusability and reducing the need for duplicating code.
- Simplifying Code Maintenance: Polymorphism simplifies code maintenance by allowing you to add new subclasses or implementations without modifying existing code. Since the code interacts with objects through their common interface, adding new classes that implement the interface doesn’t affect the calling code.
- Promoting Loose Coupling: Polymorphism promotes loose coupling between classes. Code that relies on interfaces instead of concrete classes is less dependent on specific implementations. This reduces interdependencies, making the code easier to modify and less prone to cascading changes.
- Enhancing Polymorphic Behavior: You can enhance polymorphic behavior by adding new subclasses or implementing new interfaces. This makes it easier to add new features or extend existing functionality without modifying the existing codebase extensively.
- Polymorphic Containers: Polymorphism enables the use of collections (e.g., lists, arrays) that can hold objects of different subclasses but share a common superclass or interface. This allows for more generic and flexible data structures.
- Dynamic Method Invocation: Polymorphism allows dynamic method invocation at runtime. The JVM determines which overridden method to call based on the actual type of the object, enabling dynamic and late binding.
- Interface-based Design: Polymorphism encourages interface-based design, where classes are defined based on contracts (interfaces) rather than specific implementations. This improves the modularity and abstraction of the code.
- Simplifying APIs and Frameworks: Polymorphism is fundamental in creating APIs and frameworks. APIs can define common interfaces that client code must adhere to, allowing developers to interact with the API in a standard way.
Overall, polymorphism is a key concept that enhances the flexibility, reusability, and maintainability of object-oriented code. It enables writing more generic, scalable, and adaptable software systems, making it an essential tool for modern software development.