为什么 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
形式来判断这些主体与 Consumer
或 Function
(在本例中)。
对于方法调用表达式,允许这样的事实:
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)
给定以下身份函数:
<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
形式来判断这些主体与 Consumer
或 Function
(在本例中)。
对于方法调用表达式,允许这样的事实:
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)