Kotlin is a strongly typed language and as such, the compiler only allows developers to assign values to variables of the correct type. What that means in English is that only whole numbers can be assigned to type Int, Strings can only be assigned to variables of type String, and so on. Attempting to assign the number three to a String variable results in a compiler error.
The typing system is incredibly advantageous and helps reduce many bugs that are known to happen in weakly typed languages. Developer tools can also more easily examine variables in strongly typed languages and provide code suggestions or check for incompatibilities. For example, when passing a variable to a function, both the compiler and the IDE will issue errors if the variable is incompatible with the method signature.
Types in strongly typed languages can’t be converted readily. In order to convert one type of object into another type, we have to use a technique called casting. Casting tells the compiler that a conversion is OK to make and we are accepting the risk of converting from one data type into another type. Let’s begin with a few classes that give us an idea of what we are working with here.
Now let’s begin with some examples of casting between different types.
//This is an upcast. Any is the supertype of all Kotlin classes so it's //acceptable to assign a Bob object to an Any reference val bob : Any = Bob() //Now let's downcast Bob to GrillCook. We use the is operator for this task. if (bob is GrillCook){ //We can now use the bob varaible as if it were GrillCook rather than Any //This is because the compiler now knows that bob is at least a GrillCook println("Bob is a GrillCook") }
The above example shows upcasting and downcasting. When we created the bob variable, we declared its type to be Any. Any is the base class of all Kotlin objects, so it’s complete acceptable to treat a Bob object as if it’s an Any. Later on, we wish to cast Bob from any Any object into a GrillCook.
The compiler has no way to verify if Bob is a GrillCook. It only knows that Bob is any Any object at this point. To make change Bob from Any into GrillCook, we have to perform a downcast. That’s done in the if statement with if (bob is GrillCook
. This statement checks the type of Bob at runtime and if Bob is a GrillCook, it will convert Bob from Any to GrillCook. The compiler will treat Bob like a GrillCook inside of the if block because it knows that if the is test succeeded, Bob must be a GrillCook.
We can also check if an object isn’t a certain kind of object. Let’s consider this code example.
val linda : Any = Linda() if (linda !is GrillCook){ //Linda will still be treated as Any because the compiler only //knows that she isn't the grill cook println("Linda is not the GrillCook) }
When we add an exclamation point in front of is to make !is, we are saying to check if the object isn’t a certain kind of object. In this case, Linda does not extend GrillCook, so she’s not of type GrillCook. We proceed into the if block but Linda is still treated as type Any. The compiler only knows that she isn’t a GrillCook, but it still doesn’t know exactly what Linda is other than Any.
We can use the when expression for casting also. Let’s take a look at Gene.
val gene : Children = Gene() when (gene) { //This line runs if gene is Gene is Gene -> println("Gene is Gene") //This line would only run if gene is Tina (he's not) is Tina -> println("Gene is Tina") //This line would only run if gene is Louise (he's not) is Louise -> { println("Gene is Louise") } }
In the case of the when statement, if the gene variable, which if of type Children, is Gene, then the is Gene -> code is executed. The gene variable will be treated as type Gene for the duration of the block because once again, the compiler knows that the variable has to be Gene at that point. The other two clauses, is Tina -> and is Louise ->, do not execute in this example. This is because the gene variable is not either Tina or Louise so the is check returns false for those two conditions.
So far, all of the casts we have performed are known as safe casts. They are safe because we have verified the type of the object at runtime and only performed the cast after it was safe to do so. However, we can perform unsafe casts. When we perform an unsafe cast, the compiler will usually issue a warning, but it will allow us to make the cast.
val teddy : Any = GrillCook() as Children
This cast will always result in a ClassCastException. That’s because GrillCook is not a Children object.
Putting it Together
package ch1.casting open class GrillCook class Bob : GrillCook() open class Wife class Linda : Wife() open class Children class Tina : Children() class Gene : Children() class Louise : Children() fun main(args : Array<String>){ //These are all upcasts. Since our variables are of the base type //we can safely assign them to child objects without the need to cast. val bob : Any = Bob() val linda : Any = Linda() val gene : Children = Gene() //This is a safe form of down casting. //If Bob is in fact a GrillCook, the next line will print //"Bob is a GrillCook" if (bob is GrillCook){ //Going forward, the compiler will treat Bob as a GrillCook object //Because it now knows that Bob is a GrillCook println("Bob is a GrillCook") } //This is the reverse of what was shown above. //The line inside of the if statement only runs when linda //is not a GrillCook. Note that the linda variable is still treated //as type Any because the compiler only knows that Linda isn't a GrillCook if (linda !is GrillCook){ println("Linda is not the GrillCook") } //We can also cast with the when expression when (gene) { //This line runs if gene is Gene is Gene -> println("Gene is Gene") //This line would only run if gene is Tina (he's not) is Tina -> println("Gene is Tina") //This line would only run if gene is Louise (he's not) is Louise -> { println("Gene is Louise") } } //This is an example of an unsafe cast. It forces the compiler //to make the conversion, but at runtime, we get a ClassCastException. //Notice that the compiler does issue a warning here, but since this statement //is valid, the code will compile val teddy : Any = GrillCook() as Children }
When run, we get the following output.
Bob is a GrillCook Linda is not the GrillCook Gene is Gene Exception in thread "main" java.lang.ClassCastException: ch1.casting.GrillCook cannot be cast to ch1.casting.Children at ch1.casting.CastingKt.main(Casting.kt:58)