下限通配符在 javac 中引起麻烦,但 Eclipse 不会

2022-09-03 02:01:25

这段代码在 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 output:

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 报告和部分讨论在这里)


答案 1

此代码是合法的 wrt JLS 8。javac 版本 8 及更早版本在处理通配符和捕获的方式上存在一些 bug。从版本9开始(抢先体验,我尝试了ea-113和更新的版本),javac也接受此代码。

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

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

的参数具有 类型 ,其中 是要通过类型推断实例化的类型变量。m2Consumer<? super T>T

在类型推断期间,使用推理变量(由 ecj 表示为 )来表示 。T#0T

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

⟨c →消费者<?超级T#0>⟩

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

⟨消费者<捕获#1-的> →消费者<?超级T#0>⟩

⟨捕获#1-的 ?<= ?超级T#0⟩

⟨T#0 <:捕获#1-of ?⟩

T#0 <: 捕获 #1-of ?

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

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

直观地说,所示的解决方案告诉我们:无论捕获可能表示什么类型,如果设置为表示完全相同的类型,则不会违反类型约束。这是可能的,因为在开始类型推断之前捕获是固定的。T


答案 2

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

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

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

但是,如果赋值 to 是有效的,则赋值 to 也是有效的。我们甚至可以通过一个中间步骤来实际证明这一点: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

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

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


推荐