推理变量具有不兼容的边界.Java 8编译器回归?

Luk*_*der 21 java type-inference compiler-errors java-8

以下程序在Java 7和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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

使用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 ?
Run Code Online (Sandbox Code Playgroud)

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

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我知道Java 8中的类型推断已经发生了很大变化(例如,由于JEP 101"广义目标类型推断").那么,这是一个错误还是一种新的语言"功能"?

编辑:我还向Oracle报告了这个JI-9021550,但是为了防止这是Java 8中的"功能",我也向Eclipse报告了这个问题:

Zho*_*gYu 7

免责声明 - 我对这个主题知之甚少,以下是我试图证明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) { ... }
Run Code Online (Sandbox Code Playgroud)

为了推断T,我们有约束

      X <: List<?>
      X <: List<T>
Run Code Online (Sandbox Code Playgroud)

从本质上讲,这是无法解决的.例如,T如果没有则存在X=List<?>.

不确定Java7如何推断这种情况.但是,javac8(和IntelliJ)表现得"合理",我会说.


现在,为什么这个解决方法有效?

    List<?> instance = type.newInstance();
    b(instance);  // ok!
Run Code Online (Sandbox Code Playgroud)

它的工作原理是通配符捕获,它引入了更多类型信息,"缩小"类型 instance

    instance is List<?>  =>  exist W, where instance is List<W>  =>  T=W
Run Code Online (Sandbox Code Playgroud)

不幸的是,这不是在什么时候完成instanceX,因此可以使用较少的类型信息.

可以想象,语言也可以"改进"以对X进行通配符捕获:

    instance is X, X is List<?>  =>  exist W, where instance is List<W>
Run Code Online (Sandbox Code Playgroud)

  • 问题只是编译器出现通配符时会发疯.只需将`<X extends List <?>>`替换为`<Y,X extends List <Y >>`,问题就完全消失了.这里的'Y`和`?`之间没有语义差异,两者都表示一个完全未知的类型,它不会向场景添加任何信息,特别是因为我们不在其他任何地方使用`Y`.仍然,使用`Y`而不是`?`使编译器感到高兴. (3认同)
  • 将`X扩展List <?>`的实例分配给`List <?>`类型的变量不是缩小类型,而是*扩展*类型.我不知道这是如何"引入更多类型信息"...... (2认同)

Ste*_*ann 6

感谢错误报告,感谢Holger,您的答案中的示例.这些和其他几个人最后让我质疑11年前在Eclipse编译器中做出的一个小改动.关键是:Eclipse非法扩展了捕获算法,以递归方式应用于通配符边界.

有一个例子,这个非法的变化完全符合Eclipse与javac的行为.与JLS中我们能够清楚地看到的一样,Eclipse开发人员已经相信这一旧决定.今天我认为以前的偏差必然有不同的原因.

今天我鼓励在这方面将ecj与JLS对齐,并且看起来很难破解的5个错误,基本上已经解决了这个问题(加上一点点调整以及补偿).

Ergo:是的,Eclipse有一个bug,但是这个bug已经修复了4.7里程碑2 :)

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

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

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

?T#0 = ??
Run Code Online (Sandbox Code Playgroud)

天真地,我们可以将类型变量解析为通配符,但是 - 可能因为通配符不被视为类型 - 减少规则将上面定义为减少为FALSE,从而使推理失败.


Hol*_*ger 5

感谢bayou.io的回答,我们可以将问题缩小到这个事实

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

产生错误的同时

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

可以编译没有问题.赋值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));
}
Run Code Online (Sandbox Code Playgroud)