重载解析和类型推断的交叉点有很多复杂性。 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 的重载方法。