I tried to rewrite the bug into a simpler example to wrap my mind around it:
public interface Helper<T extends Number> {
}
public static <T extends Number> Number nice(Helper<T> helper, T value) { // (1)
return value;
}
public static <SUB_T> Number naughty(Helper<? super SUB_T> helper, SUB_T value) { // (2)
return nice(helper, value); // (3)
}
public static Number evil(String value) {
return naughty(null, value); // (4)
}
From what I understand, the following is happening:
1: Nothing unusual here. Just defining a type parameter with a constraint and referencing another type which happens to have the same constraint.
2: I think this is the actual bug (?) - SUB_T is permitted as the subtype of a constrained type, yet itself is not subject to the constraint. This compiles.
3: The compiler derives T from pattern matching (this is the sole reason why the helper is needed)
T is inferred as the set of hypothetical types that are both subtypes of Number and supertypes of SUB_T.
4: Because SUB_T is unconstrained, we can happily instantiate SUB_T such that the set of hypothetical types is empty (e.g. set it to String).
This does mean there is no way we could create a helper object as no type would satisfy the constraints - but we don't actually need a helper object, so we can simply pass null.
In short, this bug lets the compiler infer a type hierarchy of Number -> T -> SUB_T without ensuring that Number -> SUB_T holds. Usually, that wouldn't get you far as there is no type that could substitute for T - except that you don't need a type because the compiler will accept null even for "impossible" types.
It's interesting to note that the class cast exception occurs in (3), not in (1). From what I know, that is because Java's erasure takes constraints into account - the arguments of nice() become (Helper, Number) after erasure, not (Helper, Object).
1: Nothing unusual here. Just defining a type parameter with a constraint and referencing another type which happens to have the same constraint.
2: I think this is the actual bug (?) - SUB_T is permitted as the subtype of a constrained type, yet itself is not subject to the constraint. This compiles.
3: The compiler derives T from pattern matching (this is the sole reason why the helper is needed)
T is inferred as the set of hypothetical types that are both subtypes of Number and supertypes of SUB_T.
4: Because SUB_T is unconstrained, we can happily instantiate SUB_T such that the set of hypothetical types is empty (e.g. set it to String).
This does mean there is no way we could create a helper object as no type would satisfy the constraints - but we don't actually need a helper object, so we can simply pass null.
In short, this bug lets the compiler infer a type hierarchy of Number -> T -> SUB_T without ensuring that Number -> SUB_T holds. Usually, that wouldn't get you far as there is no type that could substitute for T - except that you don't need a type because the compiler will accept null even for "impossible" types.
It's interesting to note that the class cast exception occurs in (3), not in (1). From what I know, that is because Java's erasure takes constraints into account - the arguments of nice() become (Helper, Number) after erasure, not (Helper, Object).