Java8:与lambdas和重载方法的歧义

Java8:与lambdas和重载方法的歧义

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

说我有一个功能interface A ,一个abstract class B和一个class C ,重载方法使用AB作为参数:

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.apply并将其正确解析为c.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; }
}

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

采纳答案:

在重载分辨率和类型推理的交点处有很多复杂性。 目前的lambda规范草案具有所有的血清细节。 F和G部分分别包括重载分辨率和类型推断。 我不假装一切都明白了。 引言中的总结部分是可以理解的,我建议人们阅读它们,特别是F和G部分的摘要,以了解这方面的情况。

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

假设有两种重载方法:

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

一些代码有以下方法调用:

    foo("hello");

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

我们来看这里的例子。 考虑实例中定义的接口A和抽象类B 我们有C类包含两个重载,然后一些代码调用apply方法并传递一个lambda:

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

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

两个apply重载都具有相同数量的参数。 参数是一个lambda,它必须匹配一个功能界面。 AB是实际的类型,所以表明A是一个功能接口,而B不是,所以重载分辨率的结果是apply(A) 。 在这一点上,我们现在有一个λ的目标类型A ,并为x进行类型推断。

现在的变化:

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

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

代替实际类型, apply的第二个过载是通用类型变量T 我们没有做过类型推理,所以我们不考虑T ,至少在重载分辨率完成之后才会这样。 因此,这两个重载仍然适用,也不是最具体的,编译器会发出一个错误,调用是不明确的。

您可能会认为,由于我们知道 T的类型绑定为B ,它是一个类,而不是一个功能接口,所以lambda不能应用于此过载,因此应该在重载解析过程中排除它歧义。 我不是那个有争议的人。 :-)这可能确实是编译器中的错误,也可能在规范中。

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

类型推理和超载是有争议的; 从第1天开始,类型推断的许多语言禁止重载(除了可能是真实的)。因此,对于需要推理的隐含的lambdas这样的结构来说,似乎合理的放弃一些超负荷的能力来增加可以使用隐含的lambdas的情况的范围。

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

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

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

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

问题在于,这些重载只能通过lambda返回类型进行区分,实际上我们实际上从来没有使用类型推断来使用隐式类型的lambdas。 为了使用这些,总是必须为lambda设置或提供一个显式的类型参数。 这些API后来更改为:

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

这有点笨拙,但它是完全明确的。 Stream.mapmapToDoublemapToIntmapToLong以及API周围的其他几个地方也会出现类似的情况。

底线是在类型推理的情况下获得重载分辨率是非常困难的,一般来说,语言和编译器设计师将电力从重载分辨率中消除,以使类型推理工作更好。 因此,Java 8 API避免了使用隐式类型的lambdas的重载方法。

参考更多解答:Java8:与lambdas和重载方法的歧义,转载请保留Java8:与lambdas和重载方法的歧义

更多:java