1. Overview
In this tutorial, we’ll first look at how to check the type of a given object in Kotlin. Next, we’ll learn Kotlin’s two kinds of cast approaches: the smart cast and the explicit cast.
For simplicity, we’ll use test assertions to verify our example methods’ results.
2. Type Checks
In Java, we can use the instanceof operator to check the type of a given object. For example, we can use “instanceof String” to test if an object is of the type String:
Object obj = "I am a string";
if (obj instanceof String) {
...
}
In Kotlin, we use the ‘is‘ operator to check if the given object is in a certain type. Its negated form is ‘*!is’*.
Next, let’s create a couple of Kotlin functions to address the usage of the is and !is operators:
fun isString(obj: Any): Boolean = obj is String
fun isNotString(obj: Any): Boolean = obj !is String
As the code above shows, we’ve created two functions. Both accept an argument in the Any type and check if the object obj is of type String or not.
It’s worth mentioning that Kotlin’s Any is pretty similar to Java’s Object. The only difference is that Any represents a non-nullable type.
Now, let’s create a test to verify if the functions work as expected:
val aString: Any = "I am a String"
val aLong: Any = 42L
assertThat(isString(aString)).isTrue
assertThat(isString(aLong)).isFalse
assertThat(isNotString(aString)).isFalse
assertThat(isNotString(aLong)).isTrue
The test passes if we give it a run. From the example above, we realize that the is and !is operators are pretty straightforward. Further, they’re easy to read.
However, a special type check scenario is checking types with generic type parameters. Next, let’s have a closer look at this scenario.
3. Type Checks with Type Parameters
We usually use type parameters when we work with Kotlin generics. Therefore, sometimes, we want to perform type checking with type parameters. Let’s take List
val aStringList: Any = listOf("string1", "string2", "string3")
assertThat(aStringList is List<String>).isTrue()
The above example shows we attempt to examine whether an object is a List
Kotlin: Cannot check for instance of erased type 'kotlin.collections.List<kotlin.String>'.
This approach won’t work since generic type parameters are erased at runtime. We can use List<*> to fix the issue:
assertThat(aStringList is List<*>).isTrue()
This code compiles and the test passes. However, this isn’t what we want. We aim to check if the object is a List
We know Kotlin’s reified inline function allows us to reify generic types at runtime. So, we may come up with this idea:
inline fun <reified T> Any.isType() = this is T
The isType() function is an extension function so that we can perform generic type checking:
val aStringList: Any = listOf("string1", "string2", "string3")
assertThat(aStringList.isType<List<String>>()).isTrue
If we run the test, it passes. We may think that’s the solution. However, this approach doesn’t work as expected and can give incorrect results:
val anIntList: Any = listOf(1, 2, 3)
assertThat(anIntList.isType<List<String>>()).isTrue //expect: false
As the example shows, for an integer List, the isType<List
So, we have to check elements on our own. Let’s create another extension function:
inline fun <reified T> Any.isTypedList() = this is List<*> && all { it is T }
This time, we first check if the object is a List. Further, we go through its elements and examine whether all elements are of type T. Then, we can ensure that the given object is of type List
val aStringList: Any = listOf("string1", "string2", "string3")
assertThat(aStringList.isTypedList<String>()).isTrue
Now, if we check a number List with isTypedList
val anIntList: Any = listOf(1, 2, 3)
assertThat(anIntList.isTypedList<String>()).isFalse
assertThat(anIntList.isTypedList<Int>()).isTrue
We’re using List
Next, let’s look at how Kotlin handles type casts.
4. Explicit Casts
In Java, we cast an object to the target type, using (TheType) someObject. For example, (BigDecimal) numObj casts the numObj to a BigDecimal object.
In Kotlin, we use the as and as? operators to cast types.
Next, let’s learn how to do type casting in Kotlin. Further, we’ll discuss the difference between as and as?.
as is called an unsafe cast operator. This is because *if an as-cast fails, like Java, Kotlin will throw the ClassCastException.*
An example may explain it quickly. First, let’s create a simple function to cast a given object to String and return it:
fun unsafeCastToString(obj: Any): String = obj as String
Next, let’s test it:
val aString: Any = "I am a String"
val aLong: Any = 42L
assertThat(unsafeCastToString(aString)).isEqualTo(aString)
assertFailsWith<java.lang.ClassCastException> {
unsafeCastToString(aLong)
}
As the test above shows, we pass a String and then a Long to the function. Also, we use Kotlin’s assertFailsWith function to verify if the ClassCastException is thrown.
If we execute the test, it passes.
Sometimes, if the type casting fails, we don’t want the function to throw an exception. Instead, we would like to have a null value. In Java, we can do that by catching the ClassCastException and returning null. However, in Kotlin, we can use the safe cast operator as? to achieve it.
So, again, let’s understand the usage of as? with a function and a test:
fun safeCastToString(obj: Any): String? = obj as? String
val aString: Any = "I am a String"
val aLong: Any = 42L
assertThat(unsafeCastToString(aString)).isEqualTo(aString)
assertThat(unsafeCastToString(aLong)).isNull()
As we can see in the test above, this time, when we attempt to cast a Long object to String, we’ve got a null value. Also, no exception is thrown.
5. Smart Casts
Usually, we’d like to perform casting after a successful type check. In Kotlin, if a type check is successful, the compiler tracks the type information and automatically casts the object to the target type in the scope where the ‘is‘ check is true:
val obj: Any = "..."
if (obj is String) {
// obj is smart-casted to a String
obj.subString(...)
}
Next, to better understand Kotlin’s smart cast, we’ll use it to solve a little problem.
Let’s say we receive an object (obj) in the type Any. Depending on obj‘s concrete type, we’d like to apply different operations:
- String – duplicating the string and returning obj + obj
- Long – doubling the value, returning obj * 2
- List – returning a new list containing the duplicate elements in the original list, for example, given {1 ,2, 3}, it returns {1, 2, 3, 1, 2, 3}
- other types – returning a string: “Unsupported Type Found.”
Now, let’s build a function and use smart casts to solve the problem:
fun doubleTheValue(obj: Any): Any =
when (obj) {
is String -> obj.repeat(2)
is Long -> obj * 2
is List<*> -> obj + obj
else -> "Unsupported Type Found."
}
As we can see, we can solve the problem conveniently using Kotlin’s smart casts.
As usual, let’s create a test to verify if our function works as expected:
val aString: Any = "I am a String"
val aLong: Any = 42L
val aList: Any = listOf(1, 2, 3)
val aDate: Any = Instant.now()
assertThat(doubleTheValue(aString)).isEqualTo("$aString$aString")
assertThat(doubleTheValue(aLong)).isEqualTo(84L)
assertThat(doubleTheValue(aList)).isEqualTo(listOf(1, 2, 3, 1, 2, 3))
assertThat(doubleTheValue(aDate)).isEqualTo("Unsupported Type Found")
The test passes if we run it.
6. Conclusion
In this article, we’ve learned how to perform type checks and casts in Kotlin.