6

Why is it that Java can infer the common ancestor of multiple upper-bounded types, but not of lower-bounded types?

More specifically, consider the following examples:

static class Test {

    static <T> T pick(T one, T two) {
        return two;
    }

    static void testUpperBound() {
        List<? extends Integer> extendsInteger = new ArrayList<>();

        // List<? extends Integer> is treated as a subclass of List<? extends Number>
        List<? extends Number> extendsNumber = extendsInteger;

        // List<? extends Number> is inferred as the common superclass
        extendsNumber = pick(extendsInteger, extendsNumber);
    }

    static void testLowerBound() {
        List<? super Number> superNumber = new ArrayList<>();

        // List<? super Number> is treated as a subclass of List<? super Integer>
        List<? super Integer> superInteger = superNumber;

        // The inferred common type should be List<? super Integer>,
        // but instead we get a compile error:
        superInteger = pick(superNumber, superInteger);

        // It only compiles with an explicit type argument:
        superInteger = Test.<List<? super Integer>>pick(superNumber, superInteger);
    }
}
shmosel
  • 49,289
  • 6
  • 73
  • 138

2 Answers2

2

I think I can explain why Java differentiates between a lower-bounded and upper-bounded type.

Trying to infer a common lower bound can fail when incompatible bounds are used, for example Integer and Long. When we're using an upper bound, it's always possible to find some common upper bound, in this case List<? extends Number>. But there's no common lower bound of List<? super Integer> and List<? super Long>. The only safe option in case of such a conflict would be to return List<? extends Object>, synonymous with List<?>, meaning "a List of unknown type".

Now, arguably we could have resorted to that only when there actually are conflicting bounds, as opposed to the case in my question. But maybe it was decided to take the easy way out and not assume there's a common lower bound unless explicitly specified.

shmosel
  • 49,289
  • 6
  • 73
  • 138
0

I'm using 1.8.0_25 and I'm getting the compile error. The error, however, is not that the call to pick is bad, but the variable you want to put the result into. Repeating your example:

static void testLowerBound() {
    List<? super Number> superNumber = new ArrayList<>();
    List<? super Integer> superInteger = superNumber;

    // this gets the error
    superInteger = pick(superNumber, superInteger);
    // this doesn't
    pick(superNumber, superInteger);

    // what's happening behind is
    List<? extends Object> behind = pick(superNumber, superInteger);
    superInteger = behind;
    // that last line gets the same compilation error
}

If you look at how T is being substituted in the call, the parameters are used as List, losing the information about the lower bound.

About inference: Every ? is not exactly "whatever that can be assigned to..." but "a particular type I don't want to name, that can be assigned to...". It matters because in your example you get 3 variables, 1 for each list and another, different one, for the result of pick. Now, due to the declaration of pick, the substitution for T has to satisfy the class hierarchy of the parameters. In the first case you need a substitute for <#1 extends Integer> and <#2 extends Number>. #2 could be Double, so the best clue you've got is that #3 extends Number. In the second case you need a substitute for <#1 super Integer> and <#2 super Number>. Now that means #2 could be anyone of Number, Object, Serializable; #1 adds to that list Comparable and Integer. The combinations could be Number, Object (and T should be Object); or Serializable, Integer (and T could be Serializable), so the best clue it has is that T is a List of an unknown type extending Object.

Of course it could only reach to Number, but you can't get two bounds for the same type variable, so has to let it at that

dtortola
  • 768
  • 4
  • 6
  • I don't see your reasoning. Just like we shift the bound to the nearest common superclass (`Number`) with `extends`, we should shift it to the nearest common subclass (`Integer`) with `super`. – shmosel Dec 26 '14 at 17:36