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 as an example:

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 using the ‘is‘ operator. However, this code won’t compile:

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. But this approach checks 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> reports true. Even if we employ the “reified” function, Kotlin won’t check whether each element in the List is a String.

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(), the function reports the correct result:

val anIntList: Any = listOf(1, 2, 3)
assertThat(anIntList.isTypedList<String>()).isFalse
assertThat(anIntList.isTypedList<Int>()).isTrue

We’re using List as an example in this article. If the object is a Map<K, V>, we need to check each entry’s key and value types. As we can see, there is no generic and easy way to perform this kind of type checking.

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.


原始标题:Type Checks and Casts in Kotlin