13

我在回答一个问题时遇到了一个我无法解释的场景。考虑这段代码:

interface ConsumerOne<T> {
    void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
}

class A {
    private static CustomIterable<A> iterable;
    private static List<A> aList;

    public static void main(String[] args) {
        iterable.forEach(a -> aList.add(a));     //ambiguous
        iterable.forEach(aList::add);            //ambiguous

        iterable.forEach((A a) -> aList.add(a)); //OK
    }
}

我不明白为什么显式输入 lambda 的参数(A a) -> aList.add(a)会使代码编译。此外,为什么它链接到重载 inIterable而不是 in CustomIterable
对此是否有一些解释或指向规范相关部分的链接?

注意:仅在扩展iterable.forEach((A a) -> aList.add(a));时编译(直接重载方法会导致模棱两可的错误)CustomIterable<T>Iterable<T>CustomIterable


两者兼得:

  • openjdk 版本“13.0.2” 2020-01-14
    Eclipse 编译器
  • openjdk 版本“1.8.0_232”
    Eclipse 编译器

编辑:上面的代码在使用 maven 构建时无法编译,而 Eclipse 成功编译了最后一行代码。

4

3 回答 3

8

TL;DR,这是一个编译器错误。

没有规则会在继承或默认方法时优先使用特定的适用方法。有趣的是,当我将代码更改为

interface ConsumerOne<T> {
    void accept(T a);
}
interface ConsumerTwo<T> {
  void accept(T a);
}

interface CustomIterable<T> extends Iterable<T> {
    void forEach(ConsumerOne<? super T> c); //overload
    void forEach(ConsumerTwo<? super T> c); //another overload
}

iterable.forEach((A a) -> aList.add(a));语句在 Eclipse 中产生错误。

由于在声明另一个重载时没有更改接口中forEach(Consumer<? super T) c)方法的属性,因此Iterable<T>Eclipse 选择此方法的决定不能(始终)基于该方法的任何属性。它仍然是唯一继承的方法,仍然是唯一的default方法,仍然是唯一的 JDK 方法,依此类推。无论如何,这些属性都不应该影响方法选择。

请注意,将声明更改为

interface CustomIterable<T> {
    void forEach(ConsumerOne<? super T> c);
    default void forEach(ConsumerTwo<? super T> c) {}
}

也会产生“模棱两可”的错误,因此适用的重载方法的数量也无关紧要,即使只有两个候选方法,也没有对default方法的普遍偏好。

到目前为止,当有两个适用的方法并且default涉及一个方法和一个继承关系时,问题似乎出现了,但这不是进一步挖掘的正确地方。


但是可以理解的是,您的示例的构造可能由编译器中的不同实现代码处理,一个显示错误而另一个没有。
a -> aList.add(a)是一个隐式类型的lambda 表达式,不能用于重载决议。相反,(A a) -> aList.add(a)它是一个显式类型的lambda 表达式,可用于从重载方法中选择匹配的方法,但在这里没有帮助(在这里应该没有帮助),因为所有方法都具有具有完全相同功能签名的参数类型.

作为一个反例,与

static void forEach(Consumer<String> c) {}
static void forEach(Predicate<String> c) {}
{
  forEach(s -> s.isEmpty());
  forEach((String s) -> s.isEmpty());
}

函数签名不同,使用显式类型的 lambda 表达式确实可以帮助选择正确的方法,而隐式类型的 lambda 表达式没有帮助,因此forEach(s -> s.isEmpty())会产生编译器错误。所有 Java 编译器都同意这一点。

请注意,这aList::add是一个模棱两可的方法引用,因为该add方法也被重载,所以它也不能帮助选择一个方法,但无论如何方法引用可能会被不同的代码处理。切换到明确的aList::contains或更改ListCollection明确add的,并没有改变我的 Eclipse 安装中的结果(我使用过2019-06)。

于 2020-04-22T14:56:27.290 回答
4

Eclipse 实现 JLS §15.12.2.5 的代码没有发现任何一种方法比另一种更具体,即使对于显式类型化的 lambda 也是如此。

所以理想情况下,Eclipse 会在这里停止并报告歧义。不幸的是,除了实现 JLS 之外,重载解析的实现还有不平凡的代码。据我了解,必须保留这段代码(可以追溯到 Java 5 的新版本)以填补 JLS 中的一些空白。

我已经提交了https://bugs.eclipse.org/562538来跟踪它。

独立于这个特定的错误,我只能强烈建议不要使用这种代码风格。重载有利于 Java 中的许多惊喜,再加上 lambda 类型推断,复杂性与感知收益完全不成比例。

于 2020-04-27T22:28:45.333 回答
3

Eclipse 编译器正确解析为default方法,因为根据Java 语言规范 15.12.2.5,这是最具体的方法

如果最具体的方法之一是具体的(即非abstract或默认的),那么它是最具体的方法。

javac(默认情况下由 Maven 和 IntelliJ 使用)告诉方法调用在这里是模棱两可的。但根据 Java 语言规范,这并不含糊,因为这两种方法之一是这里最具体的方法。

式类型的 lambda 表达式与 Java 中显式类型的 lambda 表达式的处理方式不同。与显式类型的 lambda 表达式相比,隐式类型的 lambda 表达式属于识别严格调用方法的第一阶段(参见Java 语言规范 jls-15.12.2.2,第一点)。因此,这里的方法调用对于隐式类型的lambda 表达式是不明确的。

在您的情况下,此错误的解决方法是指定功能接口的类型,而不是使用显式类型的 lambda 表达式,如下所示:javac

iterable.forEach((ConsumerOne<A>) aList::add);

或者

iterable.forEach((Consumer<A>) aList::add);

这是您的示例进一步最小化以进行测试:

class A {

    interface FunctionA { void f(A a); }
    interface FunctionB { void f(A a); }

    interface FooA {
        default void foo(FunctionA functionA) {}
    }

    interface FooAB extends FooA {
        void foo(FunctionB functionB);
    }

    public static void main(String[] args) {
        FooAB foo = new FooAB() {
            @Override public void foo(FunctionA functionA) {
                System.out.println("FooA::foo");
            }
            @Override public void foo(FunctionB functionB) {
                System.out.println("FooAB::foo");
            }
        };
        java.util.List<A> list = new java.util.ArrayList<A>();

        foo.foo(a -> list.add(a));      // ambiguous
        foo.foo(list::add);             // ambiguous

        foo.foo((A a) -> list.add(a));  // not ambiguous (since FooA::foo is default; javac bug)
    }

}
于 2020-04-26T14:25:24.613 回答