Kotlin - Type Erasure and Casting

Viewed 313

I'm trying to understand how a ClassCastException can be thrown from code, where as far as I understand, the type being cast to has been erased.

Example

class Foo<T : Any>(val key: String, val value: T) {
    override fun toString() = "$key: $value"
}

fun <T : Foo<*>> findByKey(items: Iterable<Foo<*>>, key: String): List<T> {
    return items.filter { it.key == key } as List<T>
}

fun main() {
    val items = listOf(
        Foo("a", "abc"), 
        Foo("a", 12345), 
        Foo("a", false)
    )
    
    val filtered = findByKey<Foo<String>>(items, "a")
    println(filtered)
    
    val values = filtered.map { it.value }
    println(values)
}

Output

[a: abc, b: 12345, c: false]

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

There are a couple of sore thumbs sticking out here:

  • Type T in findByKey is not reified, therefore erased.
  • findByKey accepts Iterable<Foo<*> rather than Iterable<T> (deliberately).
  • There's an unchecked cast to List<T>.

What I find interesting is that:

  • Calling findByKey<Foo<String>>(items, "a") doesn't throw ClassCastException.
  • In the JVM I'd expect this call to be findByKey(items, "a") as <Foo<String>> is erased.
  • Calling filtered.map { it.value } throws a ClassCastException.

I'm not looking for a solution to how to get this code working; I already know that. Rather, I'm curious to know why calling map throws a ClassCastException for a type that I believe to be erased?

2 Answers

The type isn’t yet erased at compile time. The compiler knows you have cast to List<Foo>, and has to cast the individual items to Foo to be able to get their value property in map. At runtime, the source type is not known, but the destination type needed to call its value property is known. So this cast is hard coded at compile time, before any erasure.

The exception message tells you the source type of the individual item that produced the cast error. The coded type of your List is unknown, but individual objects can be inspected to determine their type.

If you decompile your code, you will see that map function is translated into the following code:

Iterable $this$map$iv = (Iterable)filtered;
Collection destination$iv$iv = (Collection)(new ArrayList(CollectionsKt.collectionSizeOrDefault($this$map$iv, 10)));
Iterator var8 = $this$map$iv.iterator();
while(var8.hasNext()) {
   Object item$iv$iv = var8.next();
   Foo it = (Foo)item$iv$iv;
   String var13 = (String)it.getValue();
   destination$iv$iv.add(var13);
}
List values = (List)destination$iv$iv;

And this line String var13 = (String)it.getValue(); throws a ClassCastException

Related