具有多个匹配目标类型的 lambda 表达式的方法签名选择

Posted

技术标签:

【中文标题】具有多个匹配目标类型的 lambda 表达式的方法签名选择【英文标题】:Method signature selection for lambda expression with multiple matching target types 【发布时间】:2020-08-05 06:41:55 【问题描述】:

我在回答 a question 时遇到了我无法解释的情况。考虑这段代码:

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) -&gt; aList.add(a) 的参数会使代码编译。此外,为什么它链接到Iterable 中的过载而不是CustomIterable 中的过载? 是否有对此的解释或规范相关部分的链接?

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


两者都得到这个:

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

编辑:上面的代码在用maven编译时编译失败,而Eclipse编译最后一行代码成功。

【问题讨论】:

这三个都不能在 Java 8 上编译。现在我不确定这是在较新版本中修复的错误,还是引入的错误/功能......你可能应该指定 Java 版本 @Sweeper 我最初使用 jdk-13 得到了这个。 Java 8 (jdk8u232) 中的后续测试显示相同的错误。不知道为什么最后一个不能在你的机器上编译。 也无法在两个在线编译器上重现(1、2)。我在我的机器上使用 1.8.0_221。这越来越奇怪了…… @ernest_k Eclipse 有自己的编译器实现。这可能是该问题的关键信息。此外,在我看来,a clean maven build errors 最后一行应该在问题中突出显示。另一方面,关于链接的问题,可以澄清 OP 也在使用 Eclipse 的假设,因为代码不可重现。 虽然我理解这个问题的智力价值,但我只能建议不要创建仅在功能接口上有所不同的重载方法,并期望它可以安全地称为传递 lambda。我不相信 lambda 类型推断和重载的组合是普通程序员能够接近理解的东西。这是一个包含很多用户无法控制的变量的方程。请避免这种情况:) 【参考方案1】:

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) -&gt; aList.add(a)); 语句在 Eclipse 中产生错误。

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

请注意,将声明更改为

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

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

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


但可以理解的是,您的示例的构造可能由编译器中的不同实现代码处理,一个显示错误而另一个没有。a -&gt; aList.add(a) 是一个隐式类型 lambda 表达式,不能用于重载决议。相比之下,(A a) -&gt; 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 -&gt; s.isEmpty()) 会产生编译器错误。并且所有 Java 编译器都同意这一点。

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

【讨论】:

@howlger 您的评论毫无意义。默认方法继承的方法,方法是重载的。没有其他方法。继承的方法是default 方法这一事实只是一个附加点。我的回答确实已经显示了一个示例,其中 Eclipse not 优先于默认方法。 @howlger 我们的行为有根本的不同。您只发现删除 default 会改变结果并立即假设找到了观察到的行为的原因。你对此过于自信,以至于你认为其他答案是错误的,尽管它们甚至并不矛盾。因为您将自己的行为投射到其他我从未声称继承是原因之上。我证明不是。我演示了这种行为是不一致的,因为 Eclipse 在一个场景中选择了特定方法,但在另一个场景中没有选择,其中存在三个重载。 @howlger 除此之外,我已经在this comment的末尾命名了另一个场景,创建一个接口,没有继承,两个方法,一个default另一个abstract,有两个消费者喜欢争论并尝试一下。 Eclipse 正确地说它是模棱两可的,尽管一个是 default 方法。显然继承仍然与这个 Eclipse 错误有关,但与你不同的是,我不会发疯并称其他答案错误,只是因为他们没有全面分析错误。这不是我们的工作。 @howlger 不,关键是这是一个错误。大多数读者甚至不会关心细节。 Eclipse 可以在每次选择方法时掷骰子,没关系。当方法不明确时,Eclipse 不应该选择它,所以它为什么选择一个方法并不重要。这个答案证明了行为是不一致的,这已经足以强烈表明它是一个错误。没有必要指出 Eclipse 源代码中出错的那一行。这不是 *** 的目的。也许,您将 *** 与 Eclipse 的错误跟踪器混淆了。 @howlger 你再次错误地声称我做了一个(错误的)陈述,说明为什么 Eclipse 做出了错误的选择。同样,我没有,因为 eclipse 根本不应该做出选择。方法是模棱两可的。观点。我之所以使用术语“inherited”,是因为区分同名方法是必要的。我本可以说“default method”,而不改变逻辑。更准确地说,我应该使用短语“Eclipse 出于某种原因错误选择的方法”。您可以互换使用这三个短语中的任何一个,并且逻辑不会改变。【参考方案2】:

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

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

我已提交 https://bugs.eclipse.org/562538 以跟踪此问题。

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

【讨论】:

【参考方案3】:

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

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

javac(由 Maven 和 IntelliJ 默认使用)告诉方法调用在这里是不明确的。但根据 Java 语言规范,这并没有歧义,因为这两种方法之一是这里最具体的方法。

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

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

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)
    


【讨论】:

你错过了引用句子之前的前提条件:“如果所有最大特定方法都有覆盖等效签名” 当然,两个参数完全不相关的接口的方法可以没有覆盖等效的签名。除此之外,这也无法解释为什么当存在三个候选方法或两个方法都在同一个接口中声明时,Eclipse 会停止选择 default 方法。 @Holger 您的回答声称,“没有规则会在继承或默认方法时优先考虑特定适用的方法。” 我理解正确吗?你是说这个不存在的规则的前提条件在这里不适用?请注意,这里的参数是一个功能接口(见JLS 9.8)。 你断章取义地撕掉了一句话。这句话描述了 override-equivalent 方法的选择,换句话说,在所有将在运行时调用相同方法的声明之间进行选择,因为在具体类中只有一个具体方法。这与 forEach(Consumer)forEach(Consumer2) 等不同方法的情况无关,它们永远不会以相同的实现方法结束。 @StephanHerrmann 我不知道 JEP 或 JSR,但更改看起来像一个修复以符合“具体”的含义,即与 JLS§9.4 比较:“默认方法不同于在类中声明的具体方法(第 8.4.3.1 节)。”,从未改变。 @StephanHerrmann 是的,看起来从替代等效方法中选择候选人变得更加复杂,了解其背后的基本原理会很有趣,但这与手头的问题无关。应该有另一份文件解释变化和动机。过去有,但有了这个“每½年一个新版本”的政策,保持质量似乎是不可能的......

以上是关于具有多个匹配目标类型的 lambda 表达式的方法签名选择的主要内容,如果未能解决你的问题,请参考以下文章

part01.03 委托与 Lambda 表达式:委托

通过 lambda 表达式实现具有两个抽象方法的接口

Lambda表达式

函数式接口

java8函数式接口(Functional Interface)

JDK中Funtion接口源码解析和使用详解