Polymorphism in Java
Kinds of polymorphism
In general, polymorphism means that something (an object or another entity) has many forms.
Java programming language provides two types of polymorphism: static (compile-time) and dynamic (run-time) polymorphism. Static polymorphism is achieved by method overloading, while dynamic polymorphism is based on inheritance and method overriding.
The more theoretical approach subdivides the characteristics of polymorphism into three fundamentally different types:
- Ad-hoc polymorphism refers to polymorphic functions that can be applied to arguments of different types that behave differently depending on the type of the argument to which they are applied. Java supports it as method overloading.
- Subtype polymorphism (also known as subtyping) is a possibility to use an instance of a subclass when an instance of the base class is permitted.
- Parametric polymorphism is when the code is written without mention of any specific type and thus can be used transparently with any number of new types. Java supports it as generics or generic programming.
In this topic, we consider only subtype (runtime) polymorphism which is widely used in object-oriented programming.
In Java, object-oriented programming (OOP) is a programming paradigm that revolves around the concept of objects, which can represent real-world entities or abstract concepts.
Runtime polymorphic behavior
A reminder: When a subclass (child class) redefines a method of the superclass with the same signature it is known as method overriding.
Run-time polymorphism relies on two principles:
- a reference variable of the superclass can refer to any subtype object;
- a superclass method can be overridden in a subclass.
Run-time polymorphism works when an overridden method is called through the reference variable of a superclass. Java determines at runtime which version of the method (superclass/subclasses) is to be executed based on the type of the object being referred, not the type of reference. It uses a mechanism known as dynamic method dispatching.
In Java, dynamic method dispatch is a mechanism that determines which implementation of an overridden method to call at runtime, based on the actual type of the object being referred to, not the type of the reference.
Example. In the following code snippet, you can see a class hierarchy. The MythicalAnimal
superclass has two subclasses: Chimera
and Dragon
. The base class has a method named hello
. Both subclasses override this method.
class MythicalAnimal {
public void hello() {
System.out.println("Hello, I'm an unknown animal");
}
}
class Chimera extends MythicalAnimal {
@Override
public void hello() {
System.out.println("Hello! Hello!");
}
}
class Dragon extends MythicalAnimal {
@Override
public void hello() {
System.out.println("Rrrr...");
}
}
We can create a reference to the MythicalAnimal
class and assign the subclass object to it:
MythicalAnimal chimera = new Chimera();
MythicalAnimal dragon = new Dragon();
MythicalAnimal animal = new MythicalAnimal();
We can also invoke overridden methods through the base class references:
chimera.hello(); // Hello! Hello!
dragon.hello(); // Rrrr...
animal.hello(); // Hello, I'm an unknown animal
So, the result of a method call depends on the actual type of an instance, not the reference type. It's a polymorphic feature in Java. The JVM calls the appropriate method for the object that is referred to in each variable.
Subtype polymorphism allows a class to specify methods that will be common to all of its subclasses. Subtype polymorphism also makes it possible for subclasses to override the implementations of those methods. Together with abstract methods and interfaces, which you'll learn about later, subtype polymorphism is a fundamental object-oriented design concept.
Polymorphism within a class hierarchy
The same thing works with methods that are used only within a hierarchy and are not accessible from the outside.
In the following example, we have a hierarchy of files. The parent class File
, represents a description of a single file in the file system. It has a subclass named ImageFile
. It overrides the getFileInfo
method of the parent class.
class File {
protected String fullName;
// constructor with a single parameter
// getters and setters
public void printFileInfo() {
String info = this.getFileInfo(); // here is polymorphic behavior!!!
System.out.println(info);
}
protected String getFileInfo() {
return "File: " + fullName;
}
}
class ImageFile extends File {
protected int width;
protected int height;
protected byte[] content;
// constructor
// getters and setters
@Override
protected String getFileInfo() {
return String.format("Image: %s, width: %d, height: %d", fullName, width, height);
}
}
The parent class has a public method named printFileInfo
and a protected method named getFileInfo
. The second method is overridden in the subclass, but the subclass doesn't override the first method.
Let's create an instance of ImageFile
and assign it to a variable of File
.
File img = new ImageFile("/path/to/file/img.png", 480, 640, someBytes); // assigning an object
Now, when we call the method printFileInfo
, it invokes the overridden version of the method getFileInfo
.
img.printFileInfo(); // It prints "Image: /path/to/file/img.png, width: 480, height: 640"
So, run-time polymorphism allows you to invoke an overridden method of a subclass having a parent class reference. It's a powerful feature that enhances flexibility and maintainability in object-oriented programming by enabling code to be more dynamic and reusable.