Inference variable has incompatible bounds. Java 8

2019-02-05 18:52发布

问题:

The following program compiles in Java 7 and in Eclipse Mars RC2 for Java 8:

import java.util.List;

public class Test {

    static final void a(Class<? extends List<?>> type) {
        b(newList(type));
    }

    static final <T> List<T> b(List<T> list) {
        return list;
    }

    static final <L extends List<?>> L newList(Class<L> type) {
        try {
            return type.newInstance();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Using the javac 1.8.0_45 compiler, the following compilation error is reported:

Test.java:6: error: method b in class Test cannot be applied to given types;
        b(newList(type));
        ^
  required: List<T>
  found: CAP#1
  reason: inference variable L has incompatible bounds
    equality constraints: CAP#2
    upper bounds: List<CAP#3>,List<?>
  where T,L are type-variables:
    T extends Object declared in method <T>b(List<T>)
    L extends List<?> declared in method <L>newList(Class<L>)
  where CAP#1,CAP#2,CAP#3 are fresh type-variables:
    CAP#1 extends List<?> from capture of ? extends List<?>
    CAP#2 extends List<?> from capture of ? extends List<?>
    CAP#3 extends Object from capture of ?

A workaround is to locally assign a variable:

import java.util.List;

public class Test {

    static final void a(Class<? extends List<?>> type) {

        // Workaround here
        List<?> variable = newList(type);
        b(variable);
    }

    static final <T> List<T> b(List<T> list) {
        return list;
    }

    static final <L extends List<?>> L newList(Class<L> type) {
        try {
            return type.newInstance();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

I know that type inference has changed a lot in Java 8 (e.g. due to JEP 101 "generalized target-type inference"). So, is this a bug or a new language "feature"?

EDIT: I have also reported this to Oracle as JI-9021550, but just in case this is a "feature" in Java 8, I've reported the issue also to Eclipse:

  • https://bugs.eclipse.org/bugs/show_bug.cgi?id=469297

回答1:

Thanks for the bug report, and thanks, Holger, for the example in your answer. These and several others finally made me question one small change made in the Eclipse compiler 11 years ago. The point was: Eclipse had illegally extended the capture algorithm to apply recursively to wildcard bounds.

There was one example where this illegal change perfectly aligned Eclipse behavior with javac. Generations of Eclipse developers have trusted this old decision more than what we could clearly see in JLS. Today I believe that previous deviation must have had a different reason.

Today I took the courage to align ecj with JLS in this regard and voila 5 bugs that appeared to be extremely hard to crack, have essentially been solved just like that (plus a little tweak here and there as compensation).

Ergo: Yes, Eclipse had a bug, but that bug has been fixed as of 4.7 milestone 2 :)

Here's what ecj will report henceforth:

The method b(List<T>) in the type Test is not applicable for the arguments (capture#1-of ? extends List<?>)

It's the wildcard inside a capture bound that doesn't find a rule to detect compatibility. More precisely, some time during inference (incorporation to be precise) we encounter the following constraint (T#0 representing an inference variable):

⟨T#0 = ?⟩

Naively, we could just resolve the type variable to the wildcard, but -- presumably because wildcards are not considered types -- the reduction rules define the above as reducing to FALSE, thus letting inference fail.



回答2:

Disclaimer - I don't know enough about the subject, and the following is an informal reasoning of mine to try to justify javac's behavior.


We can reduce the problem to

<X extends List<?>> void a(Class<X> type) throws Exception
{
    X instance = type.newInstance();
    b(instance);  // error
}

<T> List<T> b(List<T> list) { ... }

To infer T, we have constraints

      X <: List<?>
      X <: List<T>

Essentially, this is unsolvable. For example, no T exists if X=List<?>.

Not sure how Java7 infers this case. But javac8 (and IntelliJ) behaves "reasonably", I'd say.


Now, how come this workaround works?

    List<?> instance = type.newInstance();
    b(instance);  // ok!

It works due to wildcard capture, which introduces more type info, "narrowing" the type of instance

    instance is List<?>  =>  exist W, where instance is List<W>  =>  T=W

Unfortunately, this is not done when instance is X, thus there is less type info to work with.

Conceivably, the language could be "improved" to do wildcard capture for X too:

    instance is X, X is List<?>  =>  exist W, where instance is List<W>


回答3:

Thanks to bayou.io’s answer we can narrow the problem to the fact that

<X extends List<?>> void a(X instance) {
    b(instance);  // error
}
static final <T> List<T> b(List<T> list) {
    return list;
}

produces an error while

<X extends List<?>> void a(X instance) {
    List<?> instance2=instance;
    b(instance2);
}
static final <T> List<T> b(List<T> list) {
    return list;
}

can be compiled without problems. The assignment of instance2=instance is a widening conversion which should also happen for method invocation arguments. So the difference to the pattern of this answer is the additional subtype relationship.


Note that while I’m not sure whether this specific case is in line with the Java Language Specification, some tests revealed that Eclipse accepting the code is likely due to the fact that it is more sloppy regarding Generic types in general, as the following, definitely incorrect, code could be compiled without any error nor warning:

public static void main(String... arg) {
    List<Integer> l1=Arrays.asList(0, 1, 2);
    List<String>  l2=Arrays.asList("0", "1", "2");
    a(Arrays.asList(l1, l2));
}
static final void a(List<? extends List<?>> type) {
    test(type);
}
static final <Y,L extends List<Y>> void test(List<L> type) {
    L l1=type.get(0), l2=type.get(1);
    l2.set(0, l1.get(0));
}