Polymorphism in Java

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2:

If you have a few years of experience in the Java ecosystem, and you're interested in sharing that experience with the community (and getting paid for your work of course), have a look at the "Write for Us" page.
Cheers. Eugen

1. Overview

In this article, we cover two core types of polymorphism: static or compile-time polymorphism and dynamic or runtimepolymorphism. Static polymorphism is enforced at compile time while dynamic polymorphism is realized at runtime.

2. Static Polymorphism

According to Wikipedia, static polymorphism is an imitation of polymorphism which is resolved at compile time and thus does away with run-time virtual-table lookups.

For example, our TextFile class in a file manager app can have three methods with the same signature of the read() method:

During code compilation, the compiler verifies that all invocations of the read method correspond to at least one of the three methods defined above.

3. Dynamic Polymorphism

With dynamic polymorphism, the Java Virtual Machine (JVM) handles the detection of the appropriate method to execute when a subclass is assigned to its parent form. This is necessary because the subclass may override some or all of the methods defined in the parent class.

In a hypothetical file manager app, let’s define the parent class for all files called GenericFile:

4.3. Polymorphic Parameters

Parametric polymorphism allows a name of a parameter or method in a class to be associated with different types. We have a typical example below where we define content as a String and later as an Integer:

It’s also important to note that declaration of polymorphic parameters can lead to a problem known asvariable hiding where a local declaration of a parameter always overrides the global declaration of another parameter with the same name.

To solve this problem, it is often advisable to use global references such as this keyword to point to global variables within a local context.

4.4. Polymorphic Subtypes

Polymorphic subtype conveniently makes it possible for us to assign multiple subtypes to a type and expect all invocations on the type to trigger the available definitions in the subtype.

For example, if we have a collection of GenericFiles and we invoke the getInfo() method on each of them, we can expect the output to be different depending on the subtype from which each item in the collection was derived:

Subtype polymorphism is made possible by a combination ofupcasting and late binding. Upcasting involves the casting of inheritance hierarchy from a supertype to a subtype:

ImageFile imageFile = new ImageFile();
GenericFile file = imageFile;

The resulting effect of the above is that ImageFile-specific methods cannot be invoked on the new upcast GenericFile. However, methods in the subtype override similar methods defined in the supertype.

To resolve the problem of not being able to invoke subtype-specific methods when upcasting to a supertype, we can do a downcasting of the inheritance from a supertype to a subtype. This is done by:

ImageFile imageFile = (ImageFile) file;

Late bindingstrategy helps the compiler to resolve whose method to trigger after upcasting. In the case of imageFile#getInfo vs file#getInfo in the above example, the compiler keeps a reference to ImageFile‘s getInfo method.

5. Problems with Polymorphism

Let’s look at some ambiguities in polymorphism that could potentially lead to runtime errors if not properly checked.

5.1. Type Identification During Downcasting

Recall that we earlier lost access to some subtype-specific methods after performing an upcast. Although we were able to solve this with a downcast, this does not guarantee actual type checking.

The above helps to avoid a ClassCastException exception at runtime. Another option that may be used is wrapping the cast within a try and catch block and catching the ClassCastException.

It should be noted that RTTI check is expensive due to the time and resources needed to effectively verify that a type is correct. In addition, frequent use of the instanceof keyword almost always implies a bad design.

5.2. Fragile Base Class Problem

According to Wikipedia, base or superclasses are considered fragile if seemingly safe modifications to a base class may cause derived classes to malfunction.

Let’s consider a declaration of a superclass called GenericFile and its subclass TextFile:

We observe that the above modification leaves TextFile in an infinite recursion in the writeContent() method, which eventually results in a stack overflow.

To address a fragile base class problem, we can use the final keyword to prevent subclasses from overriding the writeContent() method. Proper documentation can also help. And last but not least, the composition should generally be preferred over inheritance.

6. Conclusion

In this article, we discussed the foundational concept of polymorphism, focusing on both advantages and disadvantages.

As always, the source code for this article is available over on GitHub.

Java bottom

I just announced the new Learn Spring course, focused on the fundamentals of Spring 5 and Spring Boot 2: