Antonello Zanini I'm a software engineer, but I prefer to call myself a technology bishop. Spreading knowledge through writing is my mission.

Type casting in Kotlin: Unsafe vs. safe

7 min read 2197

Kotlin Logo Over Colorful Background

Let’s have a look at everything you need to know to start dealing with safe and unsafe Kotlin type casting like a pro.

An indispensable feature of object-oriented programming languages is the ability to convert one type to another. To enable developers to achieve this goal, these programming languages introduced the concept of type casting.

Type casting gives the ability to convert a particular type into a smaller or larger type. But as you can imagine, this transformation operation is tricky and may fail or lead to unexpected results. This is why Kotlin introduced two operators to equip you with everything you need to deal with type casting: the safe operator and the unsafe operator.

Mastering type casting in Kotlin takes time, but it plays such a crucial role in the language that every Kotlin developer should be able to employ it effectively. So, let’s dive into type casting in Kotlin and see everything you need to learn to master safe and unsafe type casts, and more.

What is type casting?

Type casting, also called type conversion, is the process of converting a variable from one data type to another. This generally happens through some operators or particular syntax. Also, in some cases, it can be performed automatically behind the scenes by the interpreter or compiler. An example of a type cast operation is the transformation of an integer into a string.

Converting a type to a smaller type is called downcasting. A smaller type is a type that occupies fewer bytes in memory or represents a child of the current type in an inheritance hierarchy. Similarly, converting a type to a larger type is called upcasting. A larger type is a type that occupies more bytes in memory or represents a parent of the current type in an inheritance hierarchy.

Keep in mind that type casting is an error-prone operation and should not be performed lightheartedly. Consequently, an unexpected cast may lead to exceptions and fatal errors. This is why you should know how to use them properly, what to expect from them, and how to prevent or handle errors when they occur.

Let’s find all this out in Kotlin.

Kotlin type casting vs. Java type casting

Since most Kotlin developers are former Java developers or still use both technologies, it is worth pointing out the differences between the two languages when it comes to type casting.

In detail, Java supports implicit type conversion from smaller to larger types. For example, an int variable can be assigned to a long variable with no explicit casts:

// this code is valid in Java
int number1 = 42;
long number2 = number1; // implicit type conversion performed 

This Java snippet is valid and results in no errors. During the assignment, Java performs an implicit type cast to convert number1 from an int to a long type. At the end of the assignment, number2 is a long type variable storing the long representation of the number1 value.

On the contrary, Kotlin does not support implicit type conversion from smaller to larger types. This means that an Int variable cannot be converted to a Long variable without an explicit cast or type conversion:

// this code is invalid in Kotlin
val number1: Int = 42
val number42: Long = number1 // Type mismatch: inferred type is Int but Long was expected

This Kotlin snippet is invalid and would lead to a “Type mismatch: inferred type is Int but Long was expected” error.

If you want to convert an Int to a Long in Kotlin, you need to perform an explicit type conversation operation through the toLong() function, as below:

// this code is valid in Kotlin
val number1: Int = 42
val number2: Long = number1.toLong()

At the end of the assignment, number2 correctly stores the Long representation of the number1 value.



Notice that Kotlin comes with several functions to convert types from smaller types to larger types and vice versa. Thus, if you want to perform type conversion on primitive types on Kotlin, consider using the conversion utility functions Kotlin equips you with.

Explicit type casting with Kotlin cast operators

Basic type casting takes place in Kotlin through explicit type casting. It is called like this because if you want to perform a type cast, you explicitly have to use one of the two following cast operators Kotlin comes with.

In detail, there are two cast operators you should be aware of:

  1. Unsafe cast operator: as
  2. Safe cast operator: as?

Let us now delve deeper into both and learn how to use them with examples and when it is best to use one or the other.

Unsafe cast operator: as

The as Kotlin cast operator is called unsafe because it throws a ClassCastException when the cast cannot be performed. In other terms, it is considered unsafe because it throws an exception whenever explicit casting is not possible.

You can use the as operator as follows:

var foo: Any = "Hello, World!"
val str1: String = foo as String
println(str1)

In this example, you are assuming that you do not know the type of the foo variable at compile time. In fact, Any is the root of the Kotlin class hierarchy, which means that every Kotlin class has Any as a superclass. In other terms, any Kotlin variable can have Any as a type.

But as you can see, foo stores a string, which is why the type casting operation performed when assigning foo to str1 through an explicit cast with as will work. When run, this snippet prints:

Hello, World!

Notice that without as, the snippet would return a “Type mismatch: inferred type is Any but String was expected” error.

But not all explicit casts are successful. Let’s have a look at this example:

var foo: Any = 42
val str1: String = foo as String
println(str1)

This code would lead to the following error:

Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String

This is why foo stores a Int value, which cannot be converted to a String type.

Similarly, the as cast operator might fail when nullable types are involved, as below:

var foo: Any? = null
val str1: String = foo as String
println(str1)

In this example, you are trying to convert a nullable type to a non-nullable type. This would lead to the “null cannot be cast to non-null type kotlin.String” error. To avoid this, simply declare str1 as a nullable type, as follows:

var foo: Any? = null
val str1: String? = foo as String?
println(str1)

In this case, the explicit cast would be performed with no error, and the following line printed in the terminal:

null

Read this article to learn everything you need to know about Kotlin null safety.

So, every time you perform an explicit cast through the as unsafe operator, you should consider that a ClassCastException might be thrown. If you want to prevent this error from crashing your application, you must handle it as follows:

var foo: Any = 42

try {
    val str1: String = foo as String  
    // ... 
} catch (e : ClassCastException) {
    println ("Cast failed!")
}

This is the only way you have to avoid errors when using the Kotlin unsafe cast operator.

Safe cast operator: as?

The as? Kotlin cast operator is called safe because it returns null when the cast cannot be performed. In other words, it is considered safe because it allows you to avoid exceptions, returning null on failure. This also means that when using it, the type of the receiver variable should always be nullable. Otherwise, an error would be thrown, as in the snippet below:

var foo: Any = "Hello, World!"
val str1: String = foo as? String
println(str1)

This example would return a “Type mismatch: inferred type is String? but String was expected” error because str1 does not have a nullable type. To make it work, all you have to do is declare str1 as String?:

var foo: Any = "Hello, World!"
val str1: String? = foo as? String
println(str1)

This would now print:

Hello, World!

So, the as? safe cast operator always requires nullable types.

Now, let’s see how it behaves in the same snippet that returned a ClassCastException with as presented above:

var foo: Any = 42
val str1: String? = foo as? String
println(str1)

This would no longer fail. On the contrary, it would print:

null

This is because 42 is an Int and cannot be cast to String, as explained before.


More great articles from LogRocket:


So, the try … catch … statement is no longer required when using the safe cast operator. On the other hand, keep in mind that the receiver type will always be nullable. So, you should always consider the case where the cast failed, and the receiver variable has null value.

You can handle the two cases as follows:

// ...

val str1: String? = foo as? String

if (str1 == null) {
    // Cast failed!

    // Here, you must access str1 attributes and methods with the ? operator
    // e.g. 
    // println(str1?.uppercase())

    // ...
} else {
    // Cast succeeded!

    // Here, you can access str1 attributes and methods directly
    // the ? operator is not required
    // e.g. 
    // println(str1.uppercase())
}

Notice that in the else branch Kotlin automatically casts str1 to a non-nullable type. This allows you to treat str1 as the explicit safe cast never happened and always had the desired type.

Unsafe cast operator vs. safe cast operator

Summing up, both unsafe and safe Kotlin cast operators allow you to perform explicit type casting.

The as unsafe operator returns an exception when the cast fails. This means that you should use it carefully and only when you are sure the cast will be successful, for example when casting a type to its supertype in an inheritance hierarchy.

On the contrary, the as? safe operator returns null when the cast fails. This means that you can use it more lightheartedly because a failed cast would not crash your application. On the other hand, it involves nullable types, and you must know how to deal with them. Also, you should avoid nullable types as they are not strictly necessary.

Implicit type casting with Kotlin smart casts

Although it is not the goal of this article, it is worth noting that Kotlin also supports implicit type casting through what is called smart casting.

Smart casting

The Kotlin smart cast feature is based on the is and !is operators. These allow you to perform a runtime check to identify whether a variable is or is not of a given type. You can use them as explained in the example below:

val foo1 : Any = "Hello, World!"

if (foo1 is String) {
println(foo1.uppercase())
}

val foo2 : Any = 42

// same as
// if !(foo is String) {
if (foo2 !is String) { 
    println("foo2 is not a String")
} 

If executed, this would print:

HELLO, WORLD!
foo2 is not a String

Now, let’s try to understand how the Kotlin smat cast feature works. In detail, the smart cast uses the is-check to infer the type of immutable variables and insert safe casts automatically at compile time when required. When this condition is met, you can avoid writing explicit type casts in your code and let the compiler write it automatically for you, as in the example below:

val foo : Any = "Hello, World!"
// println(foo.length) -> "Unresolved reference: length" error

if (foo is String) {
    // Smart cast performed!

    // foo is now a String
    println(foo.length)
}

As you can see, foo has type Any. This means that if you tried to access the length attribute, you would get an error. This is because Any does not have such an attribute. But foo stores a String. Therefore, the is-check would succeed and foo will be smart cast to String. Thus, foo can be treated as a String inside the if statement.

Wrapping up, the smart casts empower you to avoid declaring explicit casts and filling your code with useless and avoidable instructions. Understing how it works can be tricky, though. This is why you should read the page from the official Kotlin documentation and learn more about it.

Conclusion

In this article, we looked at what type casting is, how Java and Kotlin approach it, which operators Kotlin provides you with, and how, when, and why to use them. As shown, Kotlin comes with two approaches to type casting. The first one involves explicit casts and requires two operators: the as unsafe cast operator, and the as? safe cast operator.

Although looking similar, they have different meaning and behavior, especially in case of failure. So, you should never confuse them. And here we saw everything you need to start mastering them and always choosing the right operator.

The second approach to type casting involves implicit casts and is based on the smart cast Kotlin feature. This is a powerful tool to avoid filling your code with useless explicit casts and let the compiler infer the right type for you.

Thanks for reading! I hope that you found this article helpful. Feel free to reach out to me with any questions, comments, or suggestions.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

LogRocket: Instantly recreate issues in your Android apps.

LogRocket is an Android monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in your Android apps.

LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.

Start proactively monitoring your Android apps — .

.
Antonello Zanini I'm a software engineer, but I prefer to call myself a technology bishop. Spreading knowledge through writing is my mission.

Leave a Reply