为什么 Java 类型推断无法区分 Function 和 Consumer?

Why does Java type inference fail to distinguish between Function and Consumer?

给定以下身份函数:

<T> Consumer<T> f(Consumer<T> c) { return c; }          // (1)
<T,R> Function<T,R> f(Function<T, R> c) { return c; }   // (2)

我在 JDK 11 和 JDK 17 中观察到以下行为:

void _void() {}
f(x -> {});                   // okay, dispatches to (1)
f(x -> { return; });          // okay, dispatches to (1)
f(x -> { _void(); });         // okay, dispatches to (1)
f(x -> _void());              // should dispatch to (1)
|  Error:
|  reference to f is ambiguous
|    both method f(java.util.function.Function<java.lang.Object,java.lang.Object>) in  
     and method f(java.util.function.Consumer<java.lang.Object>) in  match

int _one() { return 1; }
f(x -> 1);                    // okay, dispatches to (2)
f(x -> { return 1; });        // okay, dispatches to (2)
f(x -> { return _one(); });   // okay, dispatches to (2)
f(x -> _one());               // should dispatch to (2)
|  Error:
|  reference to f is ambiguous
|    both method <T,R>f(java.util.function.Function<T,R>) in
     and method <T>f(java.util.function.Consumer<T>) in  match

为什么编译器不能使用 return 类型的表达式解析这些符号?花括号版本工作正常,我原以为它们会是更困难的情况。我知道您可以显式转换 lambda 函数,但这违背了我想要实现的目的。

x -> _void()x -> one() 预计与 Consumer<T> 兼容(one() 的结果将被丢弃)。

当 lambda 主体是块类型时,编译器会额外检查“return”兼容性。 JLS 对块体的 void/value 兼容性相当明确:

A block lambda body is void-compatible if every return statement in the block has the form return;. A block lambda body is value-compatible if it cannot complete normally (§14.21) and every return statement in the block has the form return Expression;.

虽然这并没有说明为什么 single-expression 主体会失败,但它确切说明了块主体编译的原因:编译器查看 return 形式来判断这些主体与 ConsumerFunction(在本例中)。

对于方法调用表达式,允许这样的事实:

Consumer<Integer> c = x -> one(); //discarded result
Function<T, Integer> f = x -> one(); //returned result

编译器无法解决您观察到的冲突。您可以使用块主体重写相同的 lambda 表达式来解决冲突,这只是因为块主体的检查方式不同,根据规范。


我想我想说的是更自然的问题是 “为什么块体在这种情况下完全可以编译”,因为我们通常不期望 return 类型(形式?)参与重载决议。但是 lambda 表达式与类型的一致性是另外一回事,不是吗……我认为 this(块类型有助于目标类型推断)是特殊行为。

TLDR:

编译失败的案例,编译失败主要有两个原因:

  • lambda 有一个语句表达式(在这种情况下是一个方法调用)作为它们的主体,使它们与 Consumer<T>Function<T, R> 重载兼容,
  • lambda 也是隐式类型化的,这使得它们与适用性无关,因此重载决策无法在重载之间做出决定。

让我们通过规范中的 overload resolution steps 看看它到底在哪里失败了:)

首先,让我们确定可能适用的方法。对于 x -> _void()x -> _one(),两种重载都可能适用。这是因为两个 lambda 表达式对于 Function<T, R>Consumer<T> 的函数类型都是 congruent。重要条件是:

If the lambda parameters are assumed to have the same types as the function type's parameter types, then:

  • If the function type's result is void, the lambda body is either a statement expression (§14.8) or a void-compatible block.
  • If the function type's result is a (non-void) type R, then either i) the lambda body is an expression that is compatible with R in an assignment context, or ii) the lambda body is a value-compatible block, and each result expression (§15.27.2) is compatible with R in an assignment context.

(另请注意,对于编译的情况,其中一种方法可能适用。)

然后我们尝试使用 strict invocation 解析要调用的方法。松散和可变的 arity 调用在这里不是很相关,所以如果这个阶段失败,整个事情就会失败。请注意,在该部分的开头,规范定义了“与适用性相关”,而 x -> _void()x -> _one() 均与适用性无关。这很重要。

然后我们到达:

If m is a generic method and the method invocation does not provide explicit type arguments, then the applicability of the method is inferred as specified in §18.5.1.

根据§18.5.1,要确定方法对调用的适用性,首先要根据参数和类型参数添加推理边界。那你reduce and incorporate越界了。如果结果中没有 false 边界(边界冲突时产生),则该方法适用。这里的相关点是在添加这些边界时不考虑与适用性无关的参数:

To test for applicability by strict invocation:

If k ≠ n, or if there exists an i (1 ≤ i ≤ n) such that ei is pertinent to applicability (§15.12.2.2) and either i) ei is a standalone expression of a primitive type but Fi is a reference type, or ii) Fi is a primitive type but ei is not a standalone expression of a primitive type; then the method is not applicable and there is no need to proceed with inference.

Otherwise, C includes, for all i (1 ≤ i ≤ k) where ei is pertinent to applicability, ‹ei → Fi θ›.

所以唯一添加的边界是来自类型参数的边界。他们显然不会 disagree/conflict 彼此产生 false 界限,因为它们是独立的。

同样,这两种方法都适用

当适用的方法不止一种时,我们当然会选择最具体的方法。 here 描述了为泛型方法执行此操作的过程。它很长,所以我不会在这里引用它。原则上,它类似于 §18.5.1 的工作方式 - 添加一些类型边界,如果它们彼此一致(没有 false),那么一种方法比另一种方法更具体。然而,在这种情况下,隐式类型的 lambda 导致添加 false 绑定:(


现在知道了这一点,您基本上可以通过使用与适用性相关的显式类型的 lambda 使其按您想要的方式工作。

f((Integer x) -> _one()); // (2)
f((Integer x) -> _void()); // (1)