下界通配符会在 javac 中引起问题,但不会在 Eclipse 中引起问题

Lower-bounded wild card causes trouble in javac, but not Eclipse

这段代码在 Eclipse 中编译但在 javac 中不编译:

import java.util.function.Consumer;

public class Test {
    public static final void m1(Consumer<?> c) {
        m2(c);
    }
    private static final <T> void m2(Consumer<? super T> c) {
    }
}

javac 输出:

C:\Users\lukas\workspace>javac -version
javac 1.8.0_92

C:\Users\lukas\workspace>javac Test.java
Test.java:5: error: method m2 in class Test cannot be applied to given types;
        m2(c);
        ^
  required: Consumer<? super T>
  found: Consumer<CAP#1>
  reason: cannot infer type-variable(s) T
    (argument mismatch; Consumer<CAP#1> cannot be converted to Consumer<? super T>)
  where T is a type-variable:
    T extends Object declared in method <T>m2(Consumer<? super T>)
  where CAP#1 is a fresh type-variable:
    CAP#1 extends Object from capture of ?
1 error
----------------------------------------------------------------

哪个编译器有问题,为什么? (Eclipse bug report and parts of discussion here)

这段代码在 JLS 8 中是合法的。javac 版本 8 和更早的版本在处理通配符和捕获的方式上有几个错误。从版本 9(早期访问,我尝试了 ea-113 和更新版本)开始,javac 也接受此代码。

要了解编译器如何根据 JLS 对此进行分析,必须区分什么是通配符捕获、类型变量、推理变量等。

c的类型是Consumer<capture#1-of ?>(javac会写成Consumer<CAP#1>)。此类型未知,但已修复。

m2的参数类型为Consumer<? super T>,其中T是类型推断实例化的类型变量。

在类型推断期间,一个推断变量,用ecj表示为T#0,用于表示T.

类型推断在于确定 T#0 是否可以在不违反任何给定类型约束的情况下实例化为任何类型。在这种特殊情况下,我们从这个约束开始:

⟨c → Consumer<? super T#0>⟩

逐步减少(通过应用JLS 18.2):

⟨Consumer<capture#1-of ?> → Consumer<? super T#0>⟩

⟨capture#1-of ? <= ? super T#0⟩

⟨T#0 <: capture#1-of ?⟩

T#0 <: capture#1-of ?

最后一行是“类型限制”并完成了缩减。由于不涉及进一步的约束,解析简单地将 T#0 实例化为 capture#1-of ?.

类型

通过这些步骤,类型推断已证明 m2 适用于此特定调用。 qed.

直观地,显示的解决方案告诉我们:无论捕获可能代表什么类型,如果 T 设置为代表完全相同的类型,则不会违反任何类型约束。这是可能的,因为捕获是固定的 before starting type inference.

请注意,以下内容可以毫无问题地编译:

public class Test {
    public static final void m1(Consumer<?> c) {
        m2(c);
    }
    private static final <T> void m2(Consumer<T> c) {
    }
}

尽管我们不知道消费者的实际类型,但我们知道它将可分配给 Consumer<T>,尽管我们不知道 T 是什么(不知道 T 是什么 T 是,无论如何是通用代码中的规范)。

但是如果对 Consumer<T> 的赋值是有效的,那么对 Consumer<? super T> 的赋值也是有效的。我们甚至可以通过中间步骤实际展示这一点:

public class Test {
    public static final void m1(Consumer<?> c) {
        m2(c);
    }
    private static final <T> void m2(Consumer<T> c) {
        m3(c);
    }
    private static final <T> void m3(Consumer<? super T> c) {
    }
}

没有编译器反对。

当您将通配符替换为命名类型时,它也会被接受,例如

public class Test {
    public static final void m1(Consumer<?> c) {
        m2(c);
    }
    private static final <E,T extends E> void m2(Consumer<E> c) {
    }
}

在这里,ET 的超类型,就像 ? super T 一样。

我试图找到最接近这种情况的 javac 的错误报告,但是当涉及到 javac 和通配符类型时,它们太多了,我最终放弃了。免责声明:这并不意味着有这么多的错误,只是报告了这么多相关的场景,这可能都是同一个错误的不同症状。

唯一重要的是,它已经在 Java 9 中修复。