I'm seeing a major difference in behavior between Eclipse and javac for the following code:
public class TestIncompatibleTypes {
private static <V> void libraryMethod(Class<? extends List<V>> in) {}
public static void main(String[] args) {
// Eclipse warns about 'Unchecked cast'
// Maven fails with 'incompatible types'
Class<? extends List<String>> l = (Class<? extends List<String>>) ArrayList.class;
libraryMethod(l);
}
}
Eclipse issues an 'unchecked cast' warning for the above code, but it successfully compiles. Javac generates an error:
$ java -version
openjdk version "1.8.0_72-internal"
OpenJDK Runtime Environment (build 1.8.0_72-internal-b15)
OpenJDK 64-Bit Server VM (build 25.72-b15, mixed mode)
$ javac -version
javac 1.8.0_72-internal
$ javac -source 8 TestIncompatibleTypes.java
TestIncompatibleTypes.java:13: error: incompatible types: Class<ArrayList> cannot be converted to Class<? extends List<String>>
Class<? extends List<String>> l = (Class<? extends List<String>>) ArrayList.class;
^
1 error
Can anyone explain why javac disallows the cast? My understanding is that they should be completely equivalent after type erasure.
Is there a workaround to get this to compile on both compilers?
Is there a workaround to get this to compile on both compilers?
Class<? extends List<String>> clazz;
// casting to interim type
clazz = (Class<? extends List<String>>) (Class<? extends List>) ArrayList.class;
// raw types .......
clazz = (Class) ArrayList.class;
HOWEVER! This code deserves an explanation of why it works and when it might not be safe:
java.util.ArrayList
has no particular constraints for its type variable.java.lang.Class
is immutable and can only be used to create new instances.If you use different types besides ArrayList
and Class
, this same casting can cause heap pollution by e.g. casting away important information:
class IntegerList extends ArrayList<Integer> {}
// now we can create a new instance and put String in a List<Integer>
Class<? extends List<String>> clazz =
(Class<? extends List<String>>) (Class<? extends List>) IntegerList.class;
(I'm also going to make the caveat that it could still be possible to cause heap pollution using ArrayList.class
and I just haven't thought hard enough about it.)
In Java 8, we should prefer lambdas to reflection for instance creation, which avoids this entire problem:
static <V> void libraryMethod(Supplier<? extends List<V>> in) {}
void somewhere() {
Supplier<? extends List<String>> supplier = ArrayList::new;
libraryMethod(supplier);
}
Of course if you can't use this for some reason, then you are a little stuck doing the occasional kludgy thing with Class
.
Can anyone explain why javac disallows the cast?
Java doesn't allow "sideways" casts, e.g.:
Number n = ...;
String s = (Number) n; // compiler error
I explained how this works for the raw type argument here and here.
The short explanation is that there is a subtyping relationship like this:
Class<? extends List>
╱ ╲
Class<? extends List<String>> Class<ArrayList>
Both are subtypes of Class<? extends List>
and neither one is a subtype nor a supertype of the other.