IntelliJ IDEA Java

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:

Class hierarchy in IntelliJ IDEA

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):

Run Java in IntelliJ IDEA

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.

Implement abstract class in IntelliJ IDEA

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):

Inspection in IntelliJ IDEA shows error when invalid parameter type is passed

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!

image description