Kotlin Casting

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.
Casting
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)

References

https://kotlinlang.org/docs/reference/typecasts.html

Advertisement

Kotlin Koans—Part 9

Java and Kotlin are strongly typed languages. It’s not necessary to cast types when working up an object graph. For example

public void sort(Collection col){
    //todo
}

sort(new ArrayList());
sort(new HashSet());

This is an example of polymorphism in Java. ArrayList and HashSet are both Collections so it’s acceptable to pass either types to the example sort method.

Keep in mind this is not a two way street. This code would not compile.

public void sort(List list){
    //todo
}

Collection col = new ArrayList();
sort(col); //Compile error!
sort((List) col); //OK

Even though col points at an ArrayList and ArrayList implements List, Java forbids you to pass col to sort without a cast. This is because the compiler has no idea that col is pointing at an ArrayList. Keep in mind this is true of Kotlin also.

Although we can get our code to compile with a cast, it’s still dangerous code. Let’s tweak it a little big and have col point at a HashSet instead of ArrayList.

public void sort(List list){
    //todo
}

Collection col = new HashSet();

//Compiles but throws
//ClassCastException
sort((List) col);

Now the code compiles, but it will fail at run time. There is no way to cast HashSet to a List. HashSet does not implement List in anyway so when the code attempts to make the cast, the code will fail. We have to use the instanceof operator to make sure the cast is safe first.

public void sort(List list){
    //todo
}

Collection col = new HashSet();

if (col instanceof List){
    //Now it's safe
    sort((List) col);
}

This code is now safe. It will check if the runtime type of col is a List first. If the object is a List, it will make the cast. Otherwise, the cast will not get made.

Tutorial

This portion of the Kotlin Koans tutorial shows off how Kotlin handles casting compared to Java. Here is the Java code that needs to get rewrote in Kotlin.

public class JavaCode8 extends JavaCode {
    public int eval(Expr expr) {
        if (expr instanceof Num) {
            return ((Num) expr).getValue();
        }
        if (expr instanceof Sum) {
            Sum sum = (Sum) expr;
            return eval(sum.getLeft()) + eval(sum.getRight());
        }
        throw new IllegalArgumentException("Unknown expression");
    }
}

Kotlin has a when keyword that is used for casting. Here is the equivalent Kotlin code.

fun todoTask8(expr: Expr): Int {
    when (expr) {
        is Num -> return expr.value
        is Sum -> return todoTask8(expr.left) + todoTask8(expr.right)
        else -> throw IllegalArgumentException("Unknown expression")
    }
}

As usual, Kotlin is more concise than Java. The when block starts with the when followed by the variable in question. You can have any number of is clauses in this statement followed by the type. The variable is automatically cast to the specified type on the right hand side of the -> operator.

You can click here to see Part 8

%d bloggers like this: