Java8:与 lambdas 和重载方法的歧义
Posted
技术标签:
【中文标题】Java8:与 lambdas 和重载方法的歧义【英文标题】:Java8: ambiguity with lambdas and overloaded methods 【发布时间】:2014-02-20 10:37:01 【问题描述】:我正在使用 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
,它被正确解析为 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
的子类,它不是函数式接口。为什么它不能解决正确的方法?
【问题讨论】:
你可以用c.<A> apply(x -> System.out.println(x));
显式调用第一个方法。但看起来它应该在没有它的情况下工作......
我猜答案是 B 的子类型 T 可能实现 A。
@user2580516 是的,这可能是问题所在,我没有想到这种可能性。
B 的子类型 T 可能实现 A,但它仍然不是函数式接口。
相关:***.com/q/23430854/521799
【参考方案1】:
重载解析和类型推断的交叉点有很多复杂性。 lambda 规范的current draft 包含所有血淋淋的细节。 F 节和 G 节分别涵盖重载解析和类型推断。我不假装明白这一切。不过,引言中的摘要部分相当容易理解,我建议人们阅读它们,尤其是 F 和 G 部分的摘要,以了解该领域正在发生的事情。
为了简要回顾这些问题,请考虑在存在重载方法的情况下使用一些参数进行方法调用。重载解析必须选择正确的方法来调用。方法的“形状”(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
不是,因此重载解析的结果是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));
apply
的第二个重载不是实际类型,而是泛型类型变量T
。我们还没有进行类型推断,所以我们不考虑T
,至少在重载决议完成之前是这样。因此,这两个重载仍然适用,但都不是最具体的,并且编译器会发出调用不明确的错误。
您可能会争辩说,由于我们知道 T
的类型绑定为 B
,它是一个类,而不是函数式接口,因此 lambda 不可能适用于此重载,因此在重载解决过程中应该排除它,消除歧义。我不是那个争论的人。 :-) 这可能确实是编译器甚至规范中的错误。
我知道在 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 避免了使用隐式类型 lambda 的重载方法。
【讨论】:
感谢您的全面解释。我可以总结如下:当隐式类型化的lambda表达式作为参数传递时,重载决策不考虑泛型方法的类型参数的边界? 我认为这很公平。稍微扩展一下,在重载解决过程中没有考虑到很多信息。【参考方案2】:我相信答案是 B 的子类型 T 可能实现 A,因此对于此类 T 的参数分派到哪个函数变得模棱两可。
【讨论】:
正如@StuartMarks 所评论的那样,实现 A 的 B 的子类型不是功能接口。当我尝试将 lambda 传递给使用这种类型参数化的方法时,编译器会报告错误。【参考方案3】:我认为这个测试用例暴露了 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
【讨论】:
以上是关于Java8:与 lambdas 和重载方法的歧义的主要内容,如果未能解决你的问题,请参考以下文章
Java8 新特性:Lambda 表达式方法和构造器引用Stream API新时间与日期API注解
Java8 新特性:Lambda 表达式方法和构造器引用Stream API新时间与日期API注解