Generic return type upper bound - interface vs. class - surprisingly valid code

JavaGenericsJava 8

Java Problem Overview


This is a real-world example from a 3rd party library API, but simplified.

Compiled with Oracle JDK 8u72

Consider these two methods:

<X extends CharSequence> X getCharSequence() {
    return (X) "hello";
}

<X extends String> X getString() {
    return (X) "hello";
}

Both report an "unchecked cast" warning - I get why. The thing that baffles me is why can I call

Integer x = getCharSequence();

and it compiles? The compiler should know that Integer does not implement CharSequence. The call to

Integer y = getString();

gives an error (as expected)

> incompatible types: inference variable X has incompatible upper bounds java.lang.Integer,java.lang.String

Can someone explain why would this behaviour be considered valid? How would it be useful?

The client does not know that this call is unsafe - the client's code compiles without warning. Why wouldn't the compile warn about that / issue an error?

Also, how is it different from this example:

<X extends CharSequence> void doCharSequence(List<X> l) {
}

List<CharSequence> chsL = new ArrayList<>();
doCharSequence(chsL); // compiles

List<Integer> intL = new ArrayList<>();
doCharSequence(intL); // error

Trying to pass List<Integer> gives an error, as expected:

> method doCharSequence in class generic.GenericTest cannot be applied to given types; required: java.util.List found: java.util.List reason: inference variable X has incompatible bounds equality constraints: java.lang.Integer upper bounds: java.lang.CharSequence

If that is reported as an error, why Integer x = getCharSequence(); isn't?

Java Solutions


Solution 1 - Java

CharSequence is an interface. Therefore even if SomeClass does not implement CharSequence it would be perfectly possible to create a class

class SubClass extends SomeClass implements CharSequence

Therefore you can write

SomeClass c = getCharSequence();

because the inferred type X is the intersection type SomeClass & CharSequence.

This is a bit odd in the case of Integer because Integer is final, but final doesn't play any role in these rules. For example you can write

<T extends Integer & CharSequence>

On the other hand, String is not an interface, so it would be impossible to extend SomeClass to get a subtype of String, because java does not support multiple-inheritance for classes.

With the List example, you need to remember that generics are neither covariant nor contravariant. This means that if X is a subtype of Y, List<X> is neither a subtype nor a supertype of List<Y>. Since Integer does not implement CharSequence, you cannot use List<Integer> in your doCharSequence method.

You can, however get this to compile

<T extends Integer & CharSequence> void foo(List<T> list) {
    doCharSequence(list);
}  

If you have a method that returns a List<T> like this:

static <T extends CharSequence> List<T> foo() 

you can do

List<? extends Integer> list = foo();

Again, this is because the inferred type is Integer & CharSequence and this is a subtype of Integer.

Intersection types occur implicitly when you specify multiple bounds (e.g. <T extends SomeClass & CharSequence>).

For further information, here is the part of the JLS where it explains how type bounds work. You can include multiple interfaces, e.g.

<T extends String & CharSequence & List & Comparator>

but only the first bound may be a non-interface.

Solution 2 - Java

The type that is inferred by your compiler prior to the assignment for X is Integer & CharSequence. This type feels weird, because Integer is final, but it's a perfectly valid type in Java. It is then cast to Integer, which is perfectly OK.

There is exactly one possible value for the Integer & CharSequence type: null. With the following implementation:

<X extends CharSequence> X getCharSequence() {
    return null;
}

The following assignment will work:

Integer x = getCharSequence();

Because of this possible value, there's no reason why the assignment should be wrong, even if it is obviously useless. A warning would be useful.

The real problem is the API, not the call site

In fact, I've recently blogged about this API design anti pattern. You should (almost) never design a generic method to return arbitrary types because you can (almost) never guarantee that the inferred type will be delivered. An exception are methods like Collections.emptyList(), in case of which the emptiness of the list (and generic type erasure) is the reason why any inference for <T> will work:

public static final <T> List<T> emptyList() {
    return (List<T>) EMPTY_LIST;
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAdam MichalikView Question on Stackoverflow
Solution 1 - JavaPaul BoddingtonView Answer on Stackoverflow
Solution 2 - JavaLukas EderView Answer on Stackoverflow