Java8:lambdaexpression式和重载方法的歧义
我正在玩java8 lambda,我遇到了一个我没有想到的编译器错误。
假设我有一个函数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
,并正确parsing为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
一个子类,它不是一个function接口。 为什么它不能解决正确的方法?
在重载分辨率和types推断的交集处有很多复杂性。 目前的lambda规范草案有所有的细节。 F节和G节分别涵盖重载分辨率和types推断。 我不假装理解这一切。 然而,引言中的总结部分是可以理解的,我build议人们阅读它们,特别是F和G部分的摘要,以便了解这方面的情况。
为简要概括这些问题,请考虑在存在重载方法的情况下调用某些参数的方法。 重载parsing必须select正确的方法来调用。 方法的“形状”(参数或参数)是最重要的。 显然一个方法调用一个参数不能解决一个方法,它需要两个参数。 但重载方法通常具有不同types的相同数量的参数。 在这种情况下,types开始变得重要。
假设有两个重载的方法:
void foo(int i); void foo(String s);
有些代码有以下方法调用:
foo("hello");
很明显,这解决了第二种方法,根据传递参数的types。 但是,如果我们正在做重载parsing,而且参数是lambda? (尤其是那些types是隐式的,依赖于types推断来build立types的types。)回想一下,lambdaexpression式的types是从目标types中推断出来的,也就是这个上下文中预期的types。 不幸的是,如果我们有重载方法,我们没有一个目标types,直到我们解决了我们要调用的重载方法。 但是由于我们还没有lambdaexpression式的types,所以在重载parsing时,我们不能使用它的types来帮助我们。
我们来看看这里的例子。 考虑示例中定义的接口A
和抽象类B
我们有包含两个重载的类C
,然后一些代码调用apply
方法并传递一个lambda:
public void apply(A a) public B apply(B b) c.apply(x -> System.out.println(x));
两个apply
重载都有相同数量的参数。 参数是一个lambda,它必须匹配一个function接口。 A
和B
是实际的types,所以表明A
是一个function接口,而B
不是,因此重载parsing的结果是apply(A)
。 在这一点上,我们现在有一个lambda的目标typesA
, x
types推断继续。
现在的变化:
public void apply(A a) public <T extends B> T apply(T t) c.apply(x -> System.out.println(x));
apply
的第二个重载是一个genericstypesvariablesT
,而不是实际的types。 我们还没有进行types推断,所以我们不考虑T
,至less在重载parsing完成之后才会考虑。 因此这两个重载仍然适用,也不是最具体的,并且编译器发出该呼叫不明确的错误。
你可能会争辩说,因为我们知道 T
有一个B
的types边界,它是一个类,而不是一个函数接口,lambda不可能适用于这个重载,所以它应该在重载过程中被排除,歧义。 我不是那个有争论的人。 :-)这可能确实是编译器或甚至规范中的一个错误。
我知道这个领域在Java 8的devise过程中经历了一系列的变化。早期的变化试图将更多的types检查和推理信息带入重载决策阶段,但是它们更难以实现,指定和理解。 (是的,甚至比现在更难理解。)不幸的是问题不断出现。 决定通过减less可能超载的范围来简化事情。
types推理和超载是反对的; 从第1天开始,许多types推理的语言都禁止重载(除了可能在arity上)。因此,对于需要推理的隐式lambdastypes的构造,似乎有理由放弃一些重载的东西来增加隐式lambdas可以使用的情况的范围。
– Lambda专家组的Brian Goetz,2013年8月9日
(这是一个相当有争议的决定,请注意,这个线程中有116个消息,还有其他几个线程讨论这个问题。
这个决定的后果之一是,为了避免重载,必须修改某些API,例如Comparator API 。 以前, Comparator.comparing
方法有四个重载:
comparing(Function) comparing(ToDoubleFunction) comparing(ToIntFunction) comparing(ToLongFunction)
问题是,这些重载只能通过lambda返回types来区分,而我们实际上从来没有得到过使用隐式typeslambdaexpression式的types推断。 为了使用这些,总是必须为lambda转换或提供一个明确的types参数。 这些API后来被更改为:
comparing(Function) comparingDouble(ToDoubleFunction) comparingInt(ToIntFunction) comparingLong(ToLongFunction)
这有些笨拙,但是完全没有歧义。 Stream.map
, mapToDouble
, mapToInt
和mapToLong
以及API的其他地方也会出现类似的情况。
底线是,在types推断存在的情况下获得重载分辨率是非常困难的,而语言和编译器devise人员为了使types推断更好地工作,而将重力从重载分解掉。 由于这个原因,Java 8 API避免了预期会使用隐式typeslambda的重载方法。
我相信答案是B的一个子typesT可能会实现A,从而使这个typesT的参数派遣到哪个函数是模糊的。
我认为这个testing用例公开了一种情况,javac 8编译器可以做更多的尝试抛弃一个不适用的重载候选,第二种方法:
public class C { public void apply(A x) { } public <T extends B> T apply(T x) { return x; } }
基于T永远不能实例化到一个function接口的事实。 这个案子非常有趣。 @ schenka7谢谢你问这个。 我会调查这个build议的利弊。
现在反对实现这一点的主要观点可能是这个代码有多频繁。 我想,一旦人们开始将当前Java代码转换为Java 8,find这种模式的可能性可能会更高。
另一个考虑是,如果我们开始向规范/编译器添加特殊情况,那么理解,解释和维护就会变得更加棘手。
我已经提交了这个错误报告: JDK-8046045