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.