javagenericscastingkotlinerasure

Java & Kotlin casting with generics. Losing typesafety


When coding in Kotlin/Java, I stumbled onto something rather odd while using casting and generics. It seems to be possible to have the type system believe a list is of the type List<Foo>, while it is actually a List<Object>.

Can anyone explain to me why this is possible?

Here is an example in both Kotlin and Java of the issue:

Example in Kotlin

fun <T> test(obj: Any): List<T> {
    val ts = ArrayList<T>()
    ts.add(obj as T)
    return ts
}

fun <T> test2(obj: Any): T {
    return obj as T
}

fun <T> test3(obj: Any): List<T> {
    val ts = ArrayList<T>()
    ts.add(test2(obj))
    return ts
}


fun main(args: Array<String>) {
    val x = test<Double>(1) // Returns a list of Integers and doesn't error
    println(x)

    val y = test2<Double>(1) // Casts the Int object to a Double.
    println(y)

    val z = test3<Double>(1) // Returns a list of Integers and doesn't error.
    println(z)
}

Example in Java

public class Test {
    public static <T> List<T> test(Object obj){
        ArrayList<T> ts = new ArrayList<>();
        ts.add((T) obj);
        return ts;
    }

    public static <T> T test2(Object obj){
        return (T) obj;
    }

    public static <T> List<T> test3(Object obj){
        ArrayList<T> ts = new ArrayList<>();
        ts.add(test2(obj));
        return ts;
    }


    public static void main(String[] args) {
        List<Double> x = test(1); // Returns a list of Integers and doesn't error
        System.out.println(x);

        // Double y = test2(1); // Errors in java an Integers cannot be converted into a Double.
        // System.out.println(y);

        List<Double> z = test3(1); // Returns a list of Integers and doesn't error.
        System.out.println(z);
    }
}

Solution

  • Java doesn't have reified generics. That is, generic information does not exist at runtime, and all generic code is "simplified" by a process called erasure. The compiler throws in casts when generic types are known to ensure correctness. You can't cast to a generic type, then, as generic types don't exist enough for the runtime to know whether a value is one or not, and this is why javac yells at you for doing this, because it knows you are asking the JVM to do something it cannot possibly do, introducing runtime unsafety.

    public class Test {
        public static List test(Object obj) { // generic types => erasure = raw types
            ArrayList ts = new ArrayList();
            ts.add(obj); // No cast: List.add has erasure (Ljava.lang.Object;)V
            return ts;
        }
    
        public static Object test2(Object obj) { // T is unbounded => erasure = Object
            return obj; // No cast: all types <: Object
        }
    
        public static List test3(Object obj) {
            ArrayList ts = new ArrayList();
            ts.add(test2(obj)); // Note: we don't know what T is, so we can't cast to it and ensure test2 returned one.
            return ts;
        }
    
    
        public static void main(String[] args) {
            List x = test(1); // Returns a list and doesn't error
            System.out.println(x);
    
            Double y = (Double) test2(1); // Errors in java as an Integer cannot be converted into a Double
            // This is because the compiler needs to insert casts to make generics work
            System.out.println(y);
    
            List z = test3(1);
            // Unlike y, there isn't a cast in test3 because test3 doesn't know what T is, so the Integer passes through, uncast, into a List<Double>.
            // The JVM can't detect this, because it doesn't even know what a List<Double> is.
            System.out.println(z);
        }
    }
    

    Note how test2 erases to a glorified identity function, causing test3 to do exactly the same thing as test1, but with a level of indirection.