Learn Java

Runtime Type Checking in Java

Runtime type checking

A variable of a base class can always refer to an object of a subclass. We can determine the actual type of the referred object at runtime.

Java provides several ways to do it:

  • the instanceof operator that can be used for testing if an object is of a specified type;
  • java reflection that can be used to obtain an object representing the class.

Let's consider these ways to check types of objects at runtime.

Here is a class hierarchy which we will use as an example:

class Shape {...}

class Circle extends Shape {...}

class Rectangle extends Shape {...}

The hierarchy is very simple, the fields and methods of classes are hidden for clarity. However, this hierarchy demonstrates the "IS-A" relation pretty well.

The keyword instanceof

The binary operator instanceof returns true if an object is an instance of a particular class or its subclass.

The base syntax is the following:

obj instanceof Class

We've created a couple of instances of the classes below:

Shape circle = new Circle();  // the reference is Shape, the object is Circle
Shape rect = new Rectangle(); // the reference is Shape, the object is Rectangle

Let's determine their types:

boolean circleIsCircle = circle instanceof Circle; // true
boolean circleIsRectangle = circle instanceof Rectangle; // false
boolean circleIsShape = circle instanceof Shape; // true

boolean rectIsRectangle = rect instanceof Rectangle; // true
boolean rectIsCircle = rect instanceof Circle; // false
boolean rectIsShape = rect instanceof Shape; // true

So, the instanceof keyword allows you to determine the actual type of object even if it is referred to by its superclass.

As you can see, this operator considers a subclass object an instance of the superclass:

boolean circleIsShape = circle instanceof Shape; // true

Pay attention, the type of the object in question should be a subtype (or the type) of the specified class. Otherwise, the statement cannot be compiled.

Here is a non-compiled example:

Circle c = new Circle();
boolean circleIsRect = c instanceof Rectangle; // Inconvertible types

The second line gives the compile-time error: Inconvertible types.

Pattern matching for instanceof

Since Java 14, we've had language enhancement for the instanceof operator as a preview feature, which was then finalized and officially released in Java 16. Before the release of this feature, it could only operate with a type. But now it is also able to operate with a type pattern. This provides us with a more precise syntax for type checks, followed by casting and performing certain operations. To figure out how it's useful to us, let's first look at the code without a pattern matching:

public class PatternMatchingDemo {
    public static void main(String[] args) {
        Object obj = " ";

        if (obj instanceof String) {
            String str = (String) obj;

            if (str.isBlank()) {
                System.out.println("The variable is empty or contains only a whitespace");
            }
        }
    }
}

Here we have a very simple application. If the obj variable is an instance of the String class, we cast it to String and perform a certain operation. Now take a look at the code where the new pattern is used:

public class PatternMatchingDemo {
    public static void main(String[] args) {
        Object obj = "";

        if (obj instanceof String str) {
            if (str.isBlank()) {
                System.out.println("The variable is empty or contains only a whitespace");
            }
        }
    }
}

If the statement is true, obj will be automatically cast to String and its value will be assigned to the str variable. In addition, we can combine this code with the logical && operator:

public class PatternMatchingDemo {
    public static void main(String[] args) {
        Object obj = " ";

        if (obj instanceof String str && str.length() > 0) {
            if (str.isBlank()) {
                System.out.println("The variable contains only a whitespace");
            }
        }
    }
}

The code to the right of the logical operator is executed only if the type checking returns true and the obj value is assigned to the str pattern variable. That's why the same code using the || logical operator does not compile since it does not require the type checking to return true.

Use reflection

Each object has a getClass method that can be used to obtain an object representing the class. We can directly compare the classes represented by objects at runtime using java reflection.

Let's consider an example. Here is an instance of Circle:

Shape circle = new Circle();

Let's test it using reflection:

boolean equalsCircle = circle.getClass() == Circle.class; // true
boolean equalsShape = circle.getClass() == Shape.class;   // false
boolean rectangle = circle.getClass() == Rectangle.class; // false

Unlike the instanceof operator, this approach performs strict type testing and does not see subclass objects as instances of the superclass.

There is also another way to check types. An object representing the class has the isInstance method that is similar to instanceof the keyword.

Let's test this method on circle:

boolean isInstanceOfCircle = Circle.class.isInstance(circle); // true
boolean isInstanceOfShape = Shape.class.isInstance(circle); // true
boolean isInstanceOfRectangle = Rectangle.class.isInstance(circle); // false 

Similar to the instanceof operator, this method considers a subclass object as an instance of its superclass. However, unlike the operator, the following example is successfully compiled:

Circle c = new Circle();
boolean circleIsRect = Rectangle.class.isInstance(c); // false

You can use any of the described approaches to determine the actual type of the referred object.

When to use it

If you cast a superclass object to its subclass, you may get a ClassCastException if the object has another type. Before casting, you can check the actual type using one of the approaches we've considered in this topic.

The following example demonstrates it.

Shape shape = new Circle();

if (shape.getClass() == Circle.class) {
    Circle circle = (Circle) shape;

    // now we can process it as a circle
}

Keep in mind, a lot of runtime checks in the program may indicate a poor design. Use runtime polymorphism to reduce them.

Create a free account to access the full topic

“It has all the necessary theory, lots of practice, and projects of different levels. I haven't skipped any of the 3000+ coding exercises.”
Andrei Maftei
Hyperskill Graduate