JDK 8和9中的泛型行为有所不同

34
下面是一个简单的类(可在repo中复制):
import static org.hamcrest.*;
import static org.junit.Assert.assertThat;
import java.util.*;
import org.junit.Test;

public class TestGenerics {
  @Test
  public void thisShouldCompile() {
    List<String> myList = Arrays.asList("a", "b", "c");
    assertThat("List doesn't contain unexpected elements", myList, not(anyOf(hasItem("d"), hasItem("e"), hasItem("f"))));
  }
}

行为取决于JDK版本:

  • 在JDK<=8(已测试7和8)中正常编译
  • 使用JDK 9+(已测试9、10和11 EA)编译失败

将出现以下错误:

[ERROR] /tmp/jdk-issue-generics/src/test/java/org/alostale/issues/generics/TestGenerics.java:[17,17] no suitable method found for assertThat(java.lang.String,java.util.List<java.lang.String>,org.hamcrest.Matcher<java.lang.Iterable<? super java.lang.Object>>)
    method org.junit.Assert.<T>assertThat(java.lang.String,T,org.hamcrest.Matcher<? super T>) is not applicable
      (inference variable T has incompatible bounds
        upper bounds: java.lang.String,java.lang.Object
        lower bounds: capture#1 of ? super T?,capture#2 of ? super java.lang.Object,capture#3 of ? super java.lang.Object,java.lang.Object,java.lang.String,capture#4 of ? super T?)
    method org.junit.Assert.<T>assertThat(T,org.hamcrest.Matcher<? super T>) is not applicable
      (cannot infer type-variable(s) T
        (actual and formal argument lists differ in length))

这是JDK 9中的一些预期更改还是一个bug?

我可以通过以下方式将匹配器提取到已输入的变量中,使其正常工作:

    Matcher<Iterable<? super String>> m1 = hasItem("d");
    Matcher<Iterable<? super String>> m2 = hasItem("e");
    Matcher<Iterable<? super String>> m3 = hasItem("f");
    assertThat(myList, not(anyOf(m1, m2, m3)));

但问题仍然是:是否正确,javac <=8 能够推断类型,但在9+中却不能?


1
在我看来,这肯定是一个bug。似乎有许多与JDK 9中编译泛型代码相关的修复措施:https://bugs.openjdk.java.net/browse/JDK-8177097?jql=status%20in%20(Resolved%2C%20Closed%2C%20Completed%2C%20Finalized)%20AND%20resolution%20in%20(Fixed%2C%20Delivered)%20AND%20affectedVersion%20%3D%20%229%22%20AND%20fixVersion%20%3D%20%229%22%20AND%20component%20%3D%20tools%20AND%20text%20~%20%22generics%22。 - M A
...而且junit:4.12中的Assert类是使用Java5编译的(类版本为49.0)...如果这可能会提供任何线索:) - Naman
如果我们能追踪到JLS,最好是特定的更改或错误编号,那就太好了。 - Didier L
@tkruse 你拿到公共ID了吗? - Didier L
3
已上报给 Oracle,问题编号为 https://bugs.openjdk.java.net/browse/JDK-8206142。 - tkruse
分辨率似乎被https://bugs.openjdk.java.net/browse/JDK-8016196所阻塞。 - tkruse
2个回答

12

经过一些研究,我认为我们可以排除Junit或hamcrest问题。实际上,这似乎是JDK的一个bug。以下代码将无法在JDK > 8中编译:

AnyOf<Iterable<? super String>> matcher = CoreMatchers.anyOf(
    CoreMatchers.hasItem("d"), CoreMatchers.hasItem("e"), CoreMatchers.hasItem("f"));
Error:(23, 63) java: incompatible types: inference variable T has incompatible bounds
equality constraints: java.lang.String
lower bounds: java.lang.Object,java.lang.String
将此转换为不使用任何库的MCVE:
class Test {
    class A<S> { } class B<S> { } class C<S> { } class D { }

    <T> A<B<? super T>> foo() { return null; }

    <U> C<U> bar(A<U> a1, A<? super U> a2) { return null; }

    C<B<? super D>> c = bar(foo(), foo());
}

通过在bar中使用单个变量,可以实现类似的效果,这将导致上限约束相等而不是下限:

class Test {
    class A<S> { } class B<S> { } class C<S> { } class D { }

    <T> A<B<? super T>> foo() { return null; }

    <U> C<U> bar(A<? super U> a) { return null; }

    C<B<? super D>> c = bar(foo());
}
Error:(21, 28) java: incompatible types: inference variable U has incompatible bounds
equality constraints: com.Test.B<? super com.Test.D>
upper bounds: com.Test.B<? super capture#1 of ? super com.Test.D>,java.lang.Object

看起来当 JDK 试图合理化 ? super U 时,它无法找到要使用的适当通配符类。更有趣的是,如果您完全指定 foo 的类型,则编译器实际上将成功。这对 MCVE's 和原始帖子都适用。

// This causes compile to succeed even though an IDE will call it redundant
C<B<? super D>> c = bar(this.<D>foo(), this.<D>foo());

就像你提出的情况一样,将执行过程分成多行将产生正确的结果:

A<B<? super D>> a1 = foo();
A<B<? super D>> a2 = foo();
C<B<? super D>> c = bar(a1, a2);

由于有多种编写此代码的方式应该是功能上等效的,而且只有其中一些可以编译,所以我的结论是这不是JDK的预期行为。在具有 super 绑定的通配符评估中存在某处 bug。

我的建议是针对 JDK 8 编译现有代码,并针对需要 JDK > 8 的新代码,完全指定泛型值。


说实话,我认为这很明显是编译器中的一个错误,但将其与JLS相关联并指出编译器的错误之处(或者是否有JLS中的某些更改导致了这个问题)会很有趣。 - Didier L
我将奖励你的研究成果,尽管我认为还有更多工作需要做才能真正解决问题。让我们看看 @tkruse 的错误报告会发生什么。 - Didier L
@DidierL 感谢您的慷慨奖励。我尝试查看了JLS规范中类型推断部分的差异,但没有发现任何明显的指示表明规则应该不同,而且我没有足够的知识仅通过遵循JLS规则手动验证此代码。我注意到§18.5.2与8相比有一些新的部分,特别是有一个注释: - flakes
在各种特殊情况下,基于B2中出现的边界,我们会急切地解析作为调用返回类型的推理变量。这是为了避免通常的约束“‹R θ → T›”不具备完整性保持的不幸情况。不幸的是,通过急切地解析变量,我们可能无法利用稍后推断出的边界。 - flakes

1
我创建了一个不同的MCVE,展示了类型推断上的差异:
import java.util.Arrays;
import java.util.List;


public class Example {

    public class Matcher<T> {
        private T t;
        public Matcher(T t) {
            this.t = t;
        }   
    }

    public <N> Matcher<N> anyOf(Matcher<N> first, Matcher<? super N> second) {
        return first;
    }

    public <T> Matcher<List<? super T>> hasItem1(T item) {
        return new Matcher<>(Arrays.asList(item));
    }

    public <T> Matcher<List<? super T>> hasItem2(T item) {
        return new Matcher<>(Arrays.asList(item));
    }

    public void thisShouldCompile() {
        Matcher x = (Matcher<List<? super String>>) anyOf(hasItem1("d"), hasItem2("e"));
    }
}

JDK8编译通过,JDK10会出现以下错误信息:
Example.java:27: error: incompatible types: Example.Matcher<List<? super Object>> cannot be converted to Example.Matcher<List<? super String>>
        Matcher x = (Matcher<List<? super String>>) anyOf(hasItem1("d"), hasItem2("e"));

看起来JDK10在解析NList<? super String>时存在一个Bug。

Matcher<N> anyOf(Matcher<N> first, Matcher<? super N> second)

当调用时

anyOf(Matcher<List<? super String>>, Matcher<List<? super String>>)

我建议将此问题报告给OpenJDK(在此处链接问题),并可能向hamcrest项目报告问题。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接