Java8:lambdas和重载方法的歧义

时间:2014-02-20 10:37:01

标签: java generics lambda java-8

我正在玩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的子类,它不是一个功能接口。为什么不能解决正确的方法?

3 个答案:

答案 0 :(得分:50)

在重载决策和类型推断的交叉点上存在很多复杂性。 lambda规范的current draft具有所有血腥细节。 F和G部分分别包括过载分辨率和类型推断。我不会假装理解这一切。然而,介绍中的摘要部分是可以理解的,我建议人们阅读它们,特别是F和G部分的摘要,以了解该领域的进展情况。

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

假设有两个重载方法:

    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不是,因此重载解析的结果是{{ 1}}。此时,我们现在有一个lambda的目标类型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可以&#39 ; t可能适用于此过载,因此在过载解决期间应排除它,消除歧义。我不是那个有这个论点的人。 :-)这可能确实是编译器中的错误,甚至可能是规范中的错误。

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

  

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

- Brian Goetz, Lambda Expert Group, 9 Aug 2013

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

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

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

问题是这些重载只能通过lambda返回类型来区分,而我们实际上从来没有完全使用类型推断来使用隐式类型的lambda。为了使用这些,总是必须为lambda强制转换或提供显式类型参数。这些API后来改为:

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

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

最重要的是,在存在类型推断的情况下正确地获得重载分辨率通常是非常困难的,并且语言和编译器设计者从重载分辨率中消除了功率,以便使类型推断更好地工作。因此,Java 8 API避免了使用隐式类型lambdas的重载方法。

答案 1 :(得分:4)

我认为答案是B的子类型T可能会实现A,从而使得为这种类型T的参数调度哪个函数变得模糊。

答案 2 :(得分:1)

我认为这个测试用例暴露了一种情况,其中javac 8编译器可以做更多尝试丢弃不适用的重载候选,第二种方法:

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

基于T永远不能实例化为功能接口的事实。这个案子非常有趣。 @ schenka7感谢您的提问。我将调查这样一个提案的利弊。

现在反对实现这一点的主要论点可能是此代码的频繁程度。我想,一旦人们开始将当前的Java代码转换为Java 8,找到这种模式的可能性就会更高。

另一个考虑因素是,如果我们开始在规范/编译器中添加特殊情况,那么理解,解释和维护就会变得更加棘手。

我已提交此错误报告:JDK-8046045