IntelliJ IDEA
IntelliJ IDEA – the Leading Java and Kotlin IDE, by JetBrains
Easy Hacks: How To Implement Polymorphism in Java
Polymorphism is the ability of an object to take on different forms. In programming, this means that a variable or a method can have different behaviors depending on the type of object it represents. While the term may sound intimidating to beginners, polymorphism is a powerful tool: it helps you reduce coupling, increases reusability, and helps to make your code easier to read.
Imagine you have a class called Vehicle
, and two subclasses called Car
and Bike
. All classes may have a common method called drive()
, and perhaps a property to specify the numberOfWheels
. Unless you need the specific details of Car
or Bike
, you can thus treat both as a Vehicle
throughout your code base. With polymorphism, you can write code that can handle objects of different classes in a generic way without having to know the specificities of each.
In this blog post, we’ll be looking at a practical example of polymorphism in action!
Polymorphism through inheritance
For the purposes of this post, we’re assuming you’ve already learned what inheritance is and are familiar with Java keywords such as abstract
, implements
, and extends
.
Polymorphism works in Java and other programming languages through inheritance – either by building a hierarchy of classes with one common superclass or by implementing a common interface for several classes.
To illustrate polymorphism, let’s consider the example we used in the introduction to this blog post and take a closer look at how we can use polymorphism to write code that can work with cars, bikes, and other types of vehicles.
Let’s start off with an abstract
class called Vehicle
that contains common methods and properties that all vehicles share:
public abstract class Vehicle { private final int numberOfWheels; public Vehicle(int numberOfWheels) { this.numberOfWheels = numberOfWheels; } public int getNumberOfWheels() { return numberOfWheels; } public abstract void drive(); }
While the Vehicle
class is abstract
and can’t be instantiated directly, it does specify that every subclass has to expose their numberOfWheels
, and implement a method – drive()
.
Now, let’s add two subclasses, Car
and Bike
, that extend the Vehicle
class as follows:
public class Car extends Vehicle { private final String brand; private final String model; public Car(String brand, String model) { super(4); this.brand = brand; this.model = model; } public String getBrand() { return brand; } public String getModel() { return model; } @Override public void drive() { System.out.println("Driving a car!"); } } public class Bike extends Vehicle { private final int numberOfGears; public Bike(int numberOfGears) { super(2); this.numberOfGears = numberOfGears; } public int getNumberOfGears() { return numberOfGears; } @Override public void drive() { System.out.println("Cycling along"); } }
If you look closely at the code, you’ll see Car
extends Vehicle
and also has a brand
and model
property. The constructor sets the numberOfWheels
to 4
. For Bike
, you’ll see it also extends Vehicle
and exposes its own numberOfGears
in addition to the number of gears inherited from Vehicle
. In the constructor, it sets the numberOfWheels
to 2
. Both classes also override the drive()
method, each with its own implementation.
You can use the Type Hierarchy action in IntelliJ IDEA (use ⌃H on macOS or Ctrl+H on Windows/Linux) to view the hierarchy of classes, methods, and calls and to explore the structure of source files. For the Vehicle
hierarchy and all subclasses, this is how it looks:
Now, let’s see what we can do with the resultant class hierarchy!
Polymorphism in action
You can use polymorphism to work with any Vehicle
type when no specialized properties or methods are needed. For example, you can create a Driver
class that is able to drive any Vehicle
:
public class Driver { private final String name; private final Vehicle vehicle; public Driver(String name, Vehicle vehicle) { this.name = name; this.vehicle = vehicle; } public void drive() { System.out.println(name + " is starting to drive..."); vehicle.drive(); } }
When you call the drive()
method on Driver
, it prints out the name of the driver and then invokes the drive()
method of the Vehicle
. Here’s an example program where two drivers each operate a different type of Vehicle
(a Car
and a Bike
):
Thanks to polymorphism, the drivers can simply work with the generic Vehicle
. Unless they need data or behavior that is specific to Car
or Bike
, they don’t have to do type casting or create if
/else
branches to work with different vehicle types. This definitely improves code readability!
Flexibility of polymorphism
There’s another benefit to polymorphism – flexibility! Imagine you have to introduce a new Vehicle
type into your project – Train
.
If you’re using IntelliJ IDEA, place the text caret on the Vehicle
class name, press ⌥+Enter on macOS or Alt+Enter on Windows/Linux, and select the Implement abstract class action. Give the new class a name, and then press Enter again. The IDE will then prompt which methods to implement.
If you just want to see or copy the code, here’s the newly created Train
class:
public class Train extends Vehicle { public Train(int numberOfWheels) { super(numberOfWheels); } @Override public void drive() { System.out.println("Choo choo!"); } }
Like any other Vehicle
, any Train
has the numberOfWheels
property and the drive()
method.
Now, consider the Driver
class we introduced in the previous section. As you can see from the output below, it does not need any changes in order to be able to work with Train
! You can simply create a new Driver
and ask it to drive the newly introduced vehicle.
Polymorphism helps to make your code more flexible and simplifies the process of introducing changes.
Specialized vs. generic classes
When working with polymorphism in your code base, you can avoid using specialized subclasses when all you need is the properties and methods exposed by the superclass. However, you may still want to use the more specialized classes in some cases.
An easy way to create classes and methods that require a specialized type of Vehicle
, is to use the subclass type directly. For example, a driveTrain(Train train)
method will only accept Train
and never Car
, Bike
, or any other Vehicle
type (unless you introduce a subclass of Train
):
You may also encounter cases where you do want to work with the generic Vehicle
, while still being able to look at specific properties as needed. Let’s say you have a collection of Vehicle
classes, and want to write their details to the output. Here’s how the collection would look as an ArrayList<Vehicle>
:
var vehicles = new ArrayList<Vehicle>(); vehicles.add(new Car("BMW", "1")); vehicles.add(new Bike(16)); vehicles.add(new Train(80));
If you just want to write the number of wheels for each vehicle, all you have to do is iterate over this collection, and through polymorphism, then get the value of the numberOfWheels
property:
for (var vehicle : vehicles) { System.out.printf("Number of wheels: %d%n", vehicle.getNumberOfWheels()); }
When you need the details of a specific Vehicle
type, you can check the type and use a type cast to work with a subclass such as Bike
:
for (var vehicle : vehicles) { System.out.printf("Number of wheels: %d%n", vehicle.getNumberOfWheels()); if (vehicle instanceof Bike) { var bike = (Bike) vehicle; System.out.printf("Number of gears: %d%n", bike.getNumberOfGears()); } }
Doing the type conversions for every Vehicle
type can be quite verbose, so you could also use pattern matching with a switch
expression or statement:
for (var vehicle : vehicles) { System.out.printf("Number of wheels: %d%n", vehicle.getNumberOfWheels()); switch (vehicle) { case Bike bike -> System.out.printf("Number of gears: %d%n", bike.getNumberOfGears()); case Car car -> System.out.printf("Brand and model: %s %s%n", car.getBrand(), car.getModel()); default -> System.out.println("This is just a generic Vehicle"); } }
Tip: Check out Mala Gupta’s Pattern Matching in Java blog post for more insights on pattern matching!
Summary
In this post, we’ve looked at an example of using polymorphism in Java code.
Polymorphism allows objects of different classes to be treated as the same type. By creating a hierarchy of classes, implementing a common interface, or extending a common superclass, you can write code that works with common properties and methods while still retaining the ability to work with the specialized classes when needed.
Go and give it a try in IntelliJ IDEA!