2

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?

Matthew Layton
  • 32,574
  • 37
  • 140
  • 255
  • Does this answer your question? [Java generics type erasure: when and what happens?](https://stackoverflow.com/questions/339699/java-generics-type-erasure-when-and-what-happens) – Januson Feb 13 '21 at 13:34

2 Answers2

1

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.

Tenfour04
  • 39,254
  • 8
  • 47
  • 96
  • Is it possible to obtain the type information for `String` from inside the findByKey function? – Matthew Layton Feb 13 '21 at 20:50
  • Sorry, I don't understand your question. But you can change the parameter type of `items` to `Iterable` and then you don't have to cast it to `List` at all. – Tenfour04 Feb 13 '21 at 20:53
  • There is a reason it’s `Iterable>`, because it represents a data source outside of Kotlin. What I’m asking is, if the compiler knows at some point that the result has been cast, and what it’s been cast to, can you get that type information from anywhere (without reifying the type)? – Matthew Layton Feb 13 '21 at 22:03
  • You can retrieve the class of a specific object with `::class` and you can check if its type matches something with `is`. What you cannot retrieve at runtime is generic types. So you can loop through a `List` or `List` and use `is` checks on each individual item to safely cast them. But you can never find out what the generic type of the whole List is. – Tenfour04 Feb 13 '21 at 22:13
0

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

IR42
  • 4,860
  • 1
  • 8
  • 23