The Original Article can be found on freecodecamp.org
Polymorphism allows objects to be treated in a substitutable way. This reduces duplication of code when you want the same actions to be performed on different types of objects. Polymorphism literally means “many forms”.
Let’s explain what we mean by this exactly.
If you have ever travelled internationally, one item on your packing checklist is likely to be an electrical plug adapter. Otherwise, you may not be able to charge your phone and other devices.
Bizarrely, there are approximately 16 different types of electrical sockets worldwide. Some have 2 pins, some have 3 pins, some pins are circular, some pins are rectangular, and the configuration of the pins vary.
The solution most people take is to buy a universal plug adapter.
To look at the problem another way, generally the issue is we have a socket interface which accepts only 1 type of plug object! Sockets are not polymorphic.
Life would be much easier for everyone is if we had sockets that could accept many different types of plugs. We can make the socket interface polymorphic by creating different shaped slits. You can see in the image below how this has been done.
Polymorphism helps us to create more universal interfaces.
Any object that has an IS-A relationship is considered polymorphic. You have an IS-A relationship through inheritance (using the extends keyword in the class signature), or through interfaces (using the implements keyword in the class signature).
To understand polymorphism completely, you should understand inheritance and interfaces as well.
class Dog extends Animal implements Canine{
// ... some code here
}
Based on the snippet above, a Dog
has the following IS-A relationships: Animal
, Canine
, and Object
(every class implicitly inherits from the Object class, which sounds a bit ridiculous!).
Let’s give a simple (silly) example to illustrate how we can use to polymorphism to simplify our code. We want to create an app with an interrogator that can convince any animal to talk.
We will create an Interrogator
class that is responsible for convincing the animals to talk. We don’t want to write a method for each type of animal: convinceDogToTalk(Dog dog)
, convinceCatToTalk(Cat cat)
, and so on.
We would prefer one general method that would accept any animal. How can we do this?
class Interrogator{
public static void convinceToTalk(Animal subject) {
subject.talk();
}
}
// We don't want anyone creating an animal object!
abstract class Animal {
public abstract void talk();
}
class Dog extends Animal {
public void talk() {
System.out.println("Woof!");
}
}
class Cat extends Animal {
public void talk() {
System.out.println("Meow!");
}
}
public class App {
public static void main(String[] args){
Dog dog = new Dog();
Cat cat = new Cat();
Animal animal = new Dog();
Interrogator.convinceToTalk(dog); //prints "Woof!"
Interrogator.convinceToTalk(cat); //prints "Meow!"
Interrogator.convinceToTalk(animal); //prints "Woof!"
}
}
We create the convinceToTalk
method to accept an Animal
object as a parameter. Inside the method we call the talk
method of that object. As long as the object type is an Animal
or a subclass of Animal
, the compiler is happy.
The Java Virtual Machine (JVM) decides at runtime which method will be called based on the class of the object. If the object has a type of Dog
, the JVM invokes the implementation that says “Woof!”.
This pays off in 2 ways:
Interrogator
class.This type of polymorphism is referred to as overriding.
The example we discussed already covered the broad concept of overriding. Let’s give a formal definition and more specifics.
Overriding is when you create a different implementation of the exact same instance method (identical method signature) in a related class.
At runtime, the method of the object type is chosen. This is why overriding is also referred to as runtime polymorphism.
Overriding is achieved by providing a different implementation of a method in a child class (subclass), which is defined in its parent class (superclass).
Overriding is also achieved by providing different implementations of a method defined in an interface.
Rules for overriding a method:
extends
or implements
). This is why you may find it referred to as subtype polymorphism.Recommendation: Use the @override annotation when overriding methods. It provides compile-time error-checking on the method signature. This will help you avoid breaking the rules listed above.
If you don’t want a method to be overridden, declare it as final.
class Account {
public final void withdraw(double amount) {
double newBalance = balance - amount;
if(newBalance > 0){
balance = newBalance;
}
}
}
You cannot override a static method. You are really creating an independent definition of the method in a related class.
class A {
public static void print() {
System.out.println("in A");
}
}
class B extends A {
public static void print() {
System.out.println("in B");
}
}
class Test {
public static void main(String[] args) {
A myObject = new B();
myObject.print(); // prints “in A”
}
}
Running the Test
class in the example above will print “in A”. This demonstrates overriding is not happening here.
If you change the print
method in classes A
and B
to be an instance method by removing static
from the method signature, and run the Test
class again, it will print “in B” instead! Overriding is happening now.
Remember, overriding choses the method based on the object type, not the variable type. 🧐
Overloading is when you create different versions of the same method.
The name of the method must be the same, but we can change the parameters
and return type.
In Java’s Math class, you will find many examples of overloaded methods. The max
method is overloaded for different types. In all cases, it is returning the number with the highest value from the 2 values provided, but it does it for different (unrelated) number types.
The (reference) variable type is what determines which overloaded method will be chosen. Overloading is done at compile time.
Overloaded methods provide more flexibility for people using your class. People using your class may have data in different formats, or may have different data available to them depending on different situations in their application.
For example, the List class overloads the remove
method. A List is an ordered collection of objects. So, you may want to remove an object at a particular position (index) in a list. Or you may not know the position, and just want to remove the object wherever it is. So that’s why it has 2 versions.
Constructors can be overloaded also.
For example, the Scanner class has many different inputs that can be provided for creating an object. Below is a small snapshot of the constructors that cater to this.
Rules for overloading a method:
Parameteric polymorphism is achieved through generics in Java.
Generics were added to the language in version 5.0. They were designed to extend Java’s type system to allow “a type or method to operate on objects of various types while providing compile-time type safety”.
Basically, a generic form of a class or method can have all of its types replaced.
A simple example is ArrayList. The class definition has a generic in it, and it is signified by <E>
. Some of the instance methods such as add
use this generic type in their signatures.
By providing a type in angle brackets when we create an ArrayList
object, we fill in the generic references defined throughout the class. So, if we create an ArrayList
with the Dog
generic type, the add
method will only accept a Dog
object as an argument.
There is a compile-time error if you try to add anything other than a Dog
! If you use a code editor such as IntelliJ, you will get the red squiggly line to highlight your offense (as below).
Polymorphism is a tricky topic to come to grips with, especially when you are new to programming. It takes some time to identify the right situations to use it in your code.
But once you get comfortable with it, you will find it improves your code a lot.
#java #oop #programming #developer