javagenerics

Why can I cast when using generic parameters, but not when I have specified the parameters


I do not understand why this code fails.

List<String> strings = new ArrayList<>();
List<Object> objs = (List<Object>)strings;

With the error:

|  Error:  
|  incompatible types: java.util.List<java.lang.String> cannot be converted to java.util.List<java.lang.Object>  
|      List<Object> objs = (List<Object>)strings;  
|                                        ^-----^

But the following code works.

static <T, S extends T> List<T> castIt(List<S> list){
    return (List<T>)list;
}

I would expect this to fail to compile for the same reason, but it doesn't. I can even use it with the same classes.

public static void main(String[] args){
    List<String> strings = new ArrayList<>();
    List<Object> objs = castIt(strings);
}

Solution

  • I will assume that you understand why List<String> cannot be converted to List<Object>.

    List<String> and List<Object> are provably distinct. The compiler can see that clearly these are different types.

    On the other hand, List<T> and List<S> are not provably distinct. The compiler doesn't know what exact types T and S are, based on their constraints. T and S could very well be the same type, in which case this conversion would succeed at runtime.

    This is not just because T and S are type parameters. It is the bounds of T and S that makes them not provably distinct. If the bounds were,

    <T extends String, S extends Number>
    

    Then the conversion is invalid, because T and S could not possibly be the same type, based on these constraints.

    Finally, some relevant quotes from the Java Language Specification. §5.1.6.1 (emphasis mine):

    A narrowing reference conversion exists from reference type S to reference type T if all of the following are true:

    • [...]

    • If there exists a parameterized type X that is a supertype of T, and a parameterized type Y that is a supertype of S, such that the erasures of X and Y are the same, then X and Y are not provably distinct (§4.5).

      Using types from the java.util package as an example, no narrowing reference conversion exists from ArrayList<String> to ArrayList<Object>, or vice versa, because the type arguments String and Object are provably distinct. For the same reason, no narrowing reference conversion exists from ArrayList<String> to List<Object>, or vice versa. The rejection of provably distinct types is a simple static gate to prevent "stupid" narrowing reference conversions.

    • [...]

    The definition of "provably distinct" is specified in §4.5.

    Two parameterized types are provably distinct if either of the following is true:

    • They are parameterizations of distinct generic type declarations.

    • Any of their type arguments are provably distinct.

    Two type arguments are provably distinct if one of the following is true:

    • Neither argument is a type variable or wildcard, and the two arguments are not the same type.

    • One type argument is a type variable or wildcard, with a bound (if a type variable) or an upper bound (if a wildcard, using capture conversion (§5.1.10), if necessary) of S; and the other type argument T is not a type variable or wildcard; and neither |S| <: |T| nor |T| <: |S| (§4.8, §4.10).

    • Each type argument is a type variable or wildcard, with upper bounds (from capture conversion, if necessary) of S and T; and neither |S| <: |T| nor |T| <: |S|.

    The T and S in your code are not provably distinct because of the third bullet point. T has a bound of Object, and S has the bound T. Since T is a subtype of Object, the third bullet point is not satisfied.