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);
}
}
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.