Search code examples
javaeclipsejavac

Casting generic class is warning in eclipse but error in javac


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?


Solution

  • 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.