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.
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.
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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 — try LogRocket for free.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]