Why is a cast from Double to <T : Number> possible, but not from Double to Int?
Asked Answered
B

3

5

In the following, I have a generic function fun <T : Number> sum(list : List<T>) : T with a type parameter T : Number.

In the function, I sum up the list's numbers as a sum : Double and cast the sum in the end with return sum as T.

For example, if a list of Int is passed, I also get back an Int - and this works.

fun <T : Number> sum(list : List<T>) : T {
    var sum = 0.0
    for(x in list)
        sum += x.toDouble()
    return sum as T
}
fun main() { println(sum(listOf(1,2,3))) } // prints 6

Yet, the following does not work, and I am wondering why the generic functions above works but directly casting a Double to an Int does not.

fun main() {        
    val d : Double = 6.0
    val i = d as Int // java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.Integer
    println(i)
}

I have to admit that I expected both cases to fail, but surprisingly, the generic function works and I don't know why.

So the question is: Why does the generic function work and does not throw a ClassCastException when casting from Double to Int?

Burgle answered 3/12, 2021 at 11:22 Comment(0)
C
7

Notice that in the first code snippet, which "works", you are not actually casting the result to Int. If you are using IntelliJ, it should have marked the cast as an "unchecked cast". This means that at runtime, it is not checked whether sum can actually be converted to type T. It only checks that sum is a Number, and that's it. Nothing else is performed.

You can see that the returned value is still a Double, not an Int by printing:

println(sum(listOf<Int>(1,2,3)) is Int) // false
println(sum(listOf<Int>(1,2,3)) is Double) // true

And as other answers explain, this is because of type erasure.

The reason why you still see 6, but not 6.0 is bit more involved. The Kotlin compiler sees that the sum call here should return an Int (at this point the types have not been erased yet), so it finds the println overload that takes an Int, which inlines to the Java System.out.prinln(int) method. To call this method, the compiler must generate code that converts the type-erased Number that sum returns to an int, so it calls Number.intValue.

Therefore, this is what's generated:

  33: invokestatic  #69     // Method sum:(Ljava/util/List;)Ljava/lang/Number;
  36: invokevirtual #73     // Method java/lang/Number.intValue:()I
  39: invokevirtual #79     // Method java/io/PrintStream.println:(I)V

If you had forced the compiler to call pritnln(Any), then it would print 6.0:

val any: Any = sum(listOf<Int>(1,2,3))
println(any)
Cerotype answered 3/12, 2021 at 12:11 Comment(2)
And here we have the accepted answer, thank you for highlighting the "compiler magic" that makes it print as 6!Burgle
@MarkusWeninger Perhaps I should add that, more generally, the compiler uses the Number#xxxValue()methods to unwrap wrapped numerical primitives, and the XXX.valueOf() methods to wrap primitive types into their wrappers when necessary. (Don't know if this is an implementation detail or behaviour that you can rely on though. The Kotlin/JVM spec hasn't been released the last time I checked)Cerotype
K
1

This is because the type erasure. In your case the generic info is only available during compile. At runtime the sum as T does noting, because it's not clear what T is. For example it's not possible to print out the type of T. It's an unchecked cast. You could also change the T type from Number to String, which makes no sense - but it would compile. So the function will not trow the ClassCastException because actual it does no cast. If you change the sum function to a refined type, which preserves the type info at runtime, it will do the cast and throw the error:

inline fun <reified  T : Number> sum(list: List<T>): T {
    var sum = 0.0
    for (x in list)
        sum += x.toDouble()
    return sum as T // java.lang.ClassCastException
}
Kirchner answered 3/12, 2021 at 11:44 Comment(1)
I thought that it might be due to type erasure. But to me, it is still crazy that it prints the 6 correcly (because, if no cast is performed at all, it should still be the double 6.0 "somewhere internally").Burgle
A
1

It does the cast, but the cast is to the upper bound of your generic type, which is Number

You can see this happening in the decompiled version of your code:

   public static final Number sum(@NotNull List list) {
      Intrinsics.checkNotNullParameter(list, "list");
      double sum = 0.0D;

      Number x;
      for(Iterator var4 = list.iterator(); var4.hasNext(); sum += x.doubleValue()) {
         x = (Number)var4.next();
      }

      return (Number)sum;
   }

If you look at that code, the compiler will also tell you that casting to T is redundant, because this is already ensured.

The equivalent would be:

fun main() {        
    val d : Double = 6.0
    val i = d as Number 
    println(i)
}

Which doesn't cause ClassCast exception as well.

Aboulia answered 3/12, 2021 at 12:1 Comment(2)
But if a Number is returned, how is it possible that val s : Int = sum(listOf(1,2,3)) works, i.e., the static type of the s variable is Int. (Remark: val s : Int = sum(listOf(1,2,3)) as Number does (clearly) not compile)Burgle
Also, how does printing the sum result in 6 instead of 6.0 if no cast to Int is performed?Burgle

© 2022 - 2024 — McMap. All rights reserved.