Java8:lambdas和重载方法的歧义

2022-09-01 00:35:34

我正在玩java8 lambdas,我遇到了一个我没想到的编译器错误。

假设我有一个函数式、an 和 a,其中包含重载方法,它们将 or 作为参数:interface Aabstract class Bclass CAB

public interface A { 
  void invoke(String arg); 
}

public abstract class B { 
  public abstract void invoke(String arg); 
}

public class C {
  public void apply(A x) { }    
  public B apply(B x) { return x; }
}

然后,我可以将 lambda 传递到中,并将其正确解析为 .c.applyc.apply(A)

C c = new C();
c.apply(x -> System.out.println(x));

但是,当我将作为参数的重载更改为通用版本时,编译器会报告这两个重载是不明确的。B

public class C {
  public void apply(A x) { }    
  public <T extends B> T apply(T x) { return x; }
}

我以为编译器会看到它必须是一个子类,它不是一个功能接口。为什么它不能解决正确的方法?TB


答案 1

在重载分辨率和类型推断的交集处有很多复杂性。lambda规范的当前草案包含所有血腥的细节。F和G部分分别介绍过载分辨率和类型推断。我不会假装理解这一切。不过,引言中的摘要部分是相当可以理解的,我建议人们阅读它们,特别是F和G部分的摘要,以了解这一领域的情况。

若要简要回顾这些问题,请考虑在存在重载方法的情况下使用一些参数的方法调用。重载解析必须选择要调用的正确方法。方法的“形状”(arity或参数数量)是最重要的;显然,具有一个参数的方法调用无法解析为采用两个参数的方法。但是重载方法通常具有相同数量的不同类型的参数。在这种情况下,类型开始变得重要。

假设有两个重载方法:

    void foo(int i);
    void foo(String s);

并且某些代码具有以下方法调用:

    foo("hello");

显然,这解析为第二种方法,基于所传递的参数的类型。但是,如果我们正在做重载解析,并且参数是lambda呢?(特别是类型是隐式的,它依赖于类型推断来建立类型。回想一下,lambda 表达式的类型是从目标类型(即此上下文中预期的类型)推断出来的。不幸的是,如果我们有重载方法,则在解决要调用哪个重载方法之前,我们没有目标类型。但是,由于我们还没有 lambda 表达式的类型,因此在重载解析期间,我们不能使用其类型来帮助我们。

让我们看一下这里的例子。考虑示例中定义的接口和抽象类。我们有一个包含两个重载的类,然后一些代码调用该方法并将其传递给lambda:ABCapply

    public void apply(A a)    
    public B apply(B b)

    c.apply(x -> System.out.println(x));

两个重载具有相同的参数数。该参数是一个 lambda,它必须与功能接口匹配。 并且是实际类型,因此它是函数接口而不是,因此重载解析的结果是 。此时,我们现在有一个 lambda 的目标类型,以及收益的类型推断。applyABABapply(A)Ax

现在的变化:

    public void apply(A a)    
    public <T extends B> T apply(T t)

    c.apply(x -> System.out.println(x));

的第二个重载不是实际的类型,而是泛型类型变量 。我们还没有做过类型推断,所以我们不考虑,至少在重载解析完成之前不会考虑。因此,这两个重载仍然适用,都不是最具体的,并且编译器发出一个错误,即调用是不明确的。applyTT

你可能会争辩说,由于我们知道 它具有 的类型边界 ,这是一个类,而不是函数接口,lambda 不可能应用于此重载,因此在重载解析期间应将其排除在外,从而消除歧义。我不是那个有这种争论的人。:-)这可能确实是编译器甚至规范中的错误。TB

我确实知道这个领域在Java 8的设计过程中经历了一系列变化。早期的变体确实试图将更多的类型检查和推理信息引入过载解决阶段,但它们更难实现,指定和理解。(是的,比现在更难理解。不幸的是,问题不断出现。决定通过减少可能超载的事物的范围来简化事物。

类型推断和重载永远是对立的;许多从第1天开始具有类型推断的语言都禁止重载(除了可能在arity上)。因此,对于像隐式 lambda 这样的需要推理的构造,放弃一些重载能力来增加可以使用隐式 lambda 的情况范围似乎是合理的。

-- Brian Goetz,Lambda专家组,2013年8月9日

(这是一个非常有争议的决定。请注意,此线程中有 116 条消息,还有其他几个线程讨论此问题。

此决定的后果之一是必须更改某些 API 以避免重载,例如比较器 API。以前,该方法有四个重载:Comparator.comparing

    comparing(Function)
    comparing(ToDoubleFunction)
    comparing(ToIntFunction)
    comparing(ToLongFunction)

问题在于,这些重载仅通过 lambda 返回类型进行区分,我们实际上从未完全让类型推断在这里与隐式类型的 lambda 一起工作。为了使用这些参数,必须始终为 lambda 强制转换或提供显式类型参数。这些 API 后来更改为:

    comparing(Function)
    comparingDouble(ToDoubleFunction)
    comparingInt(ToIntFunction)
    comparingLong(ToLongFunction)

这有点笨拙,但它是完全明确的。类似的情况也发生在 、 、 和 、 以及 API 周围的其他一些地方。Stream.mapmapToDoublemapToIntmapToLong

最重要的是,在存在类型推断的情况下获得正确的重载分辨率通常非常困难,并且语言和编译器设计人员为了使类型推断更好地工作而牺牲了重载分辨率的权力。因此,Java 8 API 避免了应使用隐式类型 lambda 的重载方法。


答案 2

我相信答案是B的子类型T可以实现A,从而使对于这种类型T的参数调度到哪个函数变得模糊。


推荐