Java8:lambdas和重载方法的歧义
我在玩Java8Lambdas时遇到了一个我没有预料到的编译器错误 假设我有一个功能性的Java8:lambdas和重载方法的歧义,java,generics,lambda,java-8,Java,Generics,Lambda,Java 8,我在玩Java8Lambdas时遇到了一个我没有预料到的编译器错误 假设我有一个功能性的接口a、一个抽象类B和一个类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) { }
接口a
、一个抽象类B
和一个类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)
但是,当我将以B
为参数的重载更改为泛型版本时,编译器会报告这两个重载是不明确的
public class C {
public void apply(A x) { }
public <T extends B> T apply(T x) { return x; }
}
公共C类{
公共无效应用(A x){}
公共T应用(tx){return x;}
}
我以为编译器会看到
T
必须是B
的子类,而不是函数接口。为什么它不能解决正确的方法 我相信答案是B的子类型T可能实现a,因此对于这样的类型T的参数,分配给哪个函数是不明确的。在重载解析和类型推断的交叉点上有很多复杂性。lambda规范的定义包含了所有血淋淋的细节。第F和G节分别涉及重载解析和类型推断。我并不假装完全明白。不过,导言中的摘要部分是可以理解的,我建议大家阅读它们,特别是F和G部分的摘要,以了解这方面的情况
简要回顾这些问题,考虑在重载方法存在时使用一些参数的方法调用。重载解析必须选择要调用的正确方法。方法的“形状”(arity,或参数的数量)是最重要的;显然,只有一个参数的方法调用无法解析为具有两个参数的方法。但是重载方法通常具有相同数量的不同类型的参数。在这种情况下,类型开始起作用
假设有两个重载方法: void foo(int i);
void foo(String s);
某些代码具有以下方法调用:
foo("hello");
显然,这会根据传递的参数类型解析为第二种方法。但是如果我们做的是重载解析,而这个参数是lambda呢?(特别是那些类型是隐式的,依赖类型推断来建立类型的)回想一下,lambda表达式的类型是从目标类型推断出来的,也就是说,在这个上下文中预期的类型。不幸的是,如果我们有重载方法,那么在我们解决了要调用哪个重载方法之前,我们没有目标类型。但是,因为我们还没有lambda表达式的类型,所以在重载解析期间,我们不能使用它的类型来帮助我们
让我们看看这里的例子。请考虑接口<代码> > <代码>和在示例中定义的抽象类<代码> 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
不是,因此重载解析的结果是apply(A)
。此时,我们有了lambda的目标类型a
,并且x
的类型推断继续进行
现在是变化:
public void apply(A a)
public <T extends B> T apply(T t)
c.apply(x -> System.out.println(x));
问题是,这些重载只通过lambda返回类型来区分,实际上,我们在这里从未得到过隐式类型lambda的类型推断。为了使用这些参数,必须始终为lambda强制转换或提供显式类型参数。这些API后来更改为:
comparing(Function)
comparingDouble(ToDoubleFunction)
comparingInt(ToIntFunction)
comparingLong(ToLongFunction)
这有点笨拙,但它是完全明确的。类似的情况也发生在Stream.map
、mapToDouble
、mapToInt
和mapToLong
以及API周围的一些其他地方
归根结底,在存在类型推理的情况下获得正确的重载解析通常是非常困难的,语言和编译器设计者为了使类型推理更好地工作,牺牲了重载解析的能力。因此,Java 8 API避免了使用隐式类型lambda的重载方法。我认为这个测试用例暴露了一种情况,在这种情况下,Java 8编译器可以做更多的工作来尝试放弃不适用的重载候选方法,第二种方法是:
public class C {
public void apply(A x) { }
public <T extends B> T apply(T x) { return x; }
}
公共C类{
公共无效应用(A x){}
公共T应用(tx){return x;}
}
基于T永远不能被实例化为功能接口这一事实。这个案子很有趣@谢谢你问这个问题。我会调查这项建议的利弊
现在反对实现这一点的主要论点可能是这段代码的频率有多高。我猜想,一旦人们开始将当前的Java代码转换为Java8,找到这种模式的可能性可能会更高
另一个需要考虑的问题是,如果我们开始向spec/编译器添加特殊情况,那么理解、解释和维护就会变得更加困难
我已经提交了这个bug报告:您可以使用
c显式调用第一个方法。应用(x->System.out.println(x))代码>。但是它看起来没有它应该可以工作…我猜答案是B的子类型T可能实现A。@user2580516是的,这可能是问题所在,我没有想到这种可能性。B的子类型T可能实现A,但是它仍然不是一个功能接口。相关:正如@StuartMarks评论的那样,实现a的B的子类型不是一个功能接口。当我试图将lambda传递给使用这种类型参数化的方法时,编译器报告了一个错误
comparing(Function)
comparingDouble(ToDoubleFunction)
comparingInt(ToIntFunction)
comparingLong(ToLongFunction)
public class C {
public void apply(A x) { }
public <T extends B> T apply(T x) { return x; }
}