推理变量具有不兼容的边界。 Java8 编译器回归?

Inference variable has incompatible bounds. Java 8 Compiler Regression?

以下程序在 Java 7 和 Java 8 的 Eclipse Mars RC2 中编译:

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);
        }
    }
}

使用javac 1.8.0_45编译器,编译报如下错误:

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 ?

解决方法是在本地分配一个变量:

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);
        }
    }
}

我知道类型推断在 Java 8 (e.g. due to JEP 101 "generalized target-type inference") 中发生了很大变化。那么,这是错误还是新语言 "feature"?

编辑:我也已将此作为 JI-9021550 报告给 Oracle,但以防万一这是 Java 8 中的 "feature",我也向 Eclipse 报告了这个问题:

免责声明 - 我对这个主题了解不够,以下是我的非正式推理,试图证明 javac 的行为是正确的。


我们可以将问题减少到

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

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

要推断T,我们有约束

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

本质上,这是无解的。例如,如果 X=List<?>.

则不存在 T

不确定 Java7 如何推断出这种情况。但是 javac8(和 IntelliJ)的行为 "reasonably",我会说。


现在,这个变通方法是如何工作的?

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

由于通配符捕获而起作用,它引入了更多类型信息,"narrowing" instance

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

不幸的是,当 instanceX 时不会这样做,因此可以使用的类型信息较少。

可以想象,语言也可以是 "improved" 来为 X 进行通配符捕获:

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

感谢 我们可以将问题缩小到

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

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

可以正常编译。 instance2=instance 的赋值是一个扩展转换,方法调用参数也应该发生这种情况。因此,与 模式的区别在于附加的子类型关系。


请注意,虽然我不确定这个特定案例是否符合 Java 语言规范,但一些测试表明 Eclipse 接受代码可能是因为它对一般的泛型类型,如下,肯定是不正确的,代码可以编译无错误无警告:

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));
}

感谢 bug report,也感谢 Holger 在您的回答中提供的示例。这些和其他几个最终让我质疑 11 年前在 Eclipse 编译器中所做的一个小改动。重点是:Eclipse 非法扩展了捕获算法以递归地应用于通配符边界。

有一个示例表明这一非法更改使 Eclipse 行为与 javac 完全一致。几代 Eclipse 开发人员比我们在 JLS 中可以清楚地看到的更信任这个旧决定。今天我相信之前的偏差一定是有不同的原因。

今天我鼓起勇气在这方面将 ecj 与 JLS 保持一致,瞧,看起来非常难以破解的 5 个错误基本上已经解决了(加上一些小调整作为补偿)。

因此: 是的,Eclipse 有一个错误,但该错误已在 4.7 里程碑 2 中修复:)

以下是 ecj 今后将报告的内容:

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

捕获范围内的通配符找不到检测兼容性的规则。更准确地说,在推理过程中的某个时间(准确地说是并入)我们遇到以下约束(T#0 表示推理变量):

⟨T#0 = ?⟩

天真地,我们可以将类型变量解析为通配符,但是——大概是因为通配符不被视为类型——归约规则将上述定义为归约为 FALSE,从而使推理失败。