我正在玩java8 lambdas,我遇到了一个我没想到的编译器错误。
假设我有一个功能interface A
,一个abstract class B
和一个class C
,其重载方法可以将A
或B
作为参数:
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
的子类,它不是一个功能接口。为什么不能解决正确的方法?
答案 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,它必须与功能接口匹配。 A
和B
是实际类型,因此它表明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.map
,mapToDouble
,mapToInt
和mapToLong
以及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