2

Why Java print false on this case?

I on starting write Kotlin today, and I want to cast the parameter with generics BUT the result is unexpected. And I don't know why?

fun main(args: Array<String>) {
    var t = TEST<Int, String>()
    print(t.returns("12") is Int) // print false
}
@Suppress("UNCHECKED_CAST")
class TEST<out T, in R> {
    fun returns(r: R): T {
        return r as T
    }
}

Sorry, if my english is bad.

marstran
  • 26,413
  • 5
  • 61
  • 67
  • Why is it unexpected? You're testing if a String is an Int, and it prints false. What's surprising? Note that you explicitly chose to ignore the unchecked cast warning, that precisely tells you that casting r to T doesn't do any type check. – JB Nizet Jun 29 '17 at 11:10
  • I would say that class can not have reified type parameters, so at runtime this code will be roughly equivalent to `returns(Object r){return (Object)r;}` and `print("12" instanceof Integer)`, which is false. – Oleg Estekhin Jun 29 '17 at 11:11
  • [Java has the same behavior](https://stackoverflow.com/questions/34562435/when-is-generic-return-value-of-function-casted-after-type-erasure) – Tamas Hegedus Jun 29 '17 at 11:55

3 Answers3

3

Because of how generics work on the JVM, this occurs because you ignore the warning given by @Suppress("UNCHECKED_CAST").

Simply put the TEST class compiles to the following java class:

class TEST {
    Object returns(Object r) {
        return r;
    }
}

Because generics are not reified during runtime, this means that casts with generics are meaningless.

One way to get around this is by using kotlin's reified generics, an inline function and an anonymous class to force a real cast, instead of one that gets simplified to Object:

open class TEST<out T, in R> {
    open fun returns(r: R): T {
        return r as T
    }
}

inline fun <reified T, R> createTest(): TEST<T,R> {
    return object : TEST<T,R>() {
        override fun returns(r: R): T {
            return r as T
        }
    }
}

fun main(args: Array<String>) {
    var t = createTest<Int, String>()
    print(t.returns("12") is Int) // Causes classcastexception
}
Kiskae
  • 24,655
  • 2
  • 77
  • 74
1

Java generics have type erasure. This makes it hard for JVM languages to implement reified generics. Some languages like Scala do, some languages like Kotlin do not.

This unchecked cast is really unchecked, hence the warning. Without reified generics you can't do a proper check, and the compiler will only do the typecheck at the boundary of generic and non-generic code, at runtime. Because your example doesn't really have a non-generic part, no such typecheck is done. The is expression doesn't require the left hand side to be of any specific type, so even the runtime check is omitted, just like in Java.

Consider the following example where the return value is stored in a variable:

fun main(args: Array<String>) {
    var t = TEST<Int, String>()
    var k = t.returns("12") // runtime error here
    print(k is Int)
}

@Suppress("UNCHECKED_CAST")
class TEST<out T, in R> {
    fun returns(r: R): T {
        return r as T
    }
}
...
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
    at MainKt.main(main.kt:3)

Kotlin uses Java-s type checker in many places. If you consider the equvivalent Java code, it behaves the same way:

public class JMain {
    public static void main(String[] args) {
        TEST<Integer, String> t = new TEST<>();
        System.out.println(t.returns("12") instanceof Integer); // false
    }

    static class TEST<T, R> {
        @SuppressWarnings("unchecked")
        public T returns(R r) {
            return (T)r;
        }
    }
}
Tamas Hegedus
  • 28,755
  • 12
  • 63
  • 97
  • hi, sir. the problem is not the `type erasure`. it is kotlin don't cast the result `test.returns("122")` to the actual type in runtime. – holi-java Jun 29 '17 at 11:23
  • Its pure type erasure problem, inherited from Java. See my update in minutes. – Tamas Hegedus Jun 29 '17 at 11:31
  • sir, type erasure is not type reified. – holi-java Jun 29 '17 at 11:34
  • So are you implying Kotlin should insert extra checks to prevent such misunderstandings? You are maybe right but it seems they have gone the lightweight way, mimicing Java generics – Tamas Hegedus Jun 29 '17 at 11:47
  • yes, you can see my answer again. the OP don't obtain a `ClassCastException` because he don't assign the `t.returns(...)` expression to any `Int` variable. – holi-java Jun 29 '17 at 11:49
1

The result is false rather than a ClassCastException is because the generic parameters Type Erasure occurs in compile time.

  • Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.
  • Insert type casts` if necessary to preserve type safety.

So the code above in your question the return type of the returns method is Object rather than Integer. and a String can be assign to an Object even if without additional casting. so you check a String whether is an Int which will be always false.

Another case is declare a class with bounded parameter types, which will be throws a ClassCastException. for example:

@Suppress("UNCHECKED_CAST")
class TEST<out T : Int, in R : String> {
    fun returns(r: R): T {
        return r as T
    }
}

fun main(args: Array<String>) {
    var t = TEST<Int, String>()
    print(t.returns("12") is Int) 
    //      ^--- throws ClassCastException
}

after type erasure of a generic class with bounded parameter types is like as below:

public final class TEST{
    public int returns(String value){
        return ((Integer) value);
    }
}
holi-java
  • 29,655
  • 7
  • 72
  • 83