为什么 compose() 需要显式转换而 andThen() 不需要?

Why does compose() need an explicit cast when andThen() does not?

我正在研究函数式组合,有一个例子:

Function<String, String> test = (s) -> s.concat("foo");
String str = test.andThen(String::toUpperCase).apply("bar"); 

此示例按预期编译和工作。但是,如果我使用 compose() 更改组合的顺序,则需要显式转换:

String str = test.compose((Function <String, String>) 
                          String::toUpperCase).apply("bar"); 

没有显式地将 String::toUpperCase 转换为 Function <String, String>,出现编译器错误:

Error:
incompatible types: cannot infer type-variable(s) V
    (argument mismatch; invalid method reference
      incompatible types: java.lang.Object cannot be converted to java.util.Locale)
String s = test.compose(String::toUpperCase).apply("bar");
           ^-------------------------------^

问题是为什么 compose() 需要显式转换而 andThen() 在这种情况下不需要?

因为它的定义。方法 Function#compose 定义如下:

<V> Function<V, R> compose(Function<? super V, ? extends T> before)

... 其中 V 是前函数和复合函数的额外输入类型。因此它还需要定义一个泛型参数类型,因为 Function<T, R> 只知道 TR。不需要显式转换:

String str = test.<String>compose(String::toUpperCase).apply("bar");

由于 String::toUpperCase 的歧义,干扰在这里不起作用。更不言自明的例子是 LocalDateTime -> LocalDate -> String:

的函数链
Function<LocalDate, String> test = LocalDate::toString;
String str = test.compose(LocalDateTime::toLocalDate).apply(LocalDateTime.now());

如错误消息所示,问题在于 toUpperCase 有一个接受 Locale 作为参数的重载,因此编译器错误是由于不知道哪个overload String::toUpperCase 应该是指。原则上编译器应该能够知道带有两个参数的方法引用(String 对象本身是第一个参数)不能是 Function,但我想只是没有对此的规则 - 或者更确切地说,在泛型类型参数 的推理过程中似乎没有针对此的规则,这就是显式转换解决问题的原因。

我们可以通过尝试不同的方法参考来确认过载确实是导致问题的原因,例如 String::trim,它没有过载:

// works without an explicit cast
String str = test.compose(String::trim).apply("bar");

所以现在的问题是为什么在与 andThen 组合时重载不会导致问题。不同的是andThen将两个函数按相反的顺序组合在一起,所以需要推断的类型参数是String::toUpperCaseoutput类型,即String 无论选择哪个重载。因此,当方法引用的 return 类型将用于推理时,编译器似乎有一个规则来处理类型参数推理期间重载的歧义,并且重载具有相同的 return 类型。请注意,具有相同参数类型的重载不能存在类似规则,因为重载不能具有相同的参数类型。

简答:

这是因为这些方法的工作顺序。 Funtion#andThen 方法与 Function#compose 方法相反。

详情:

Funtion#andThen 文档:

Returns a composed function that first applies this function to its input, and then applies the after function to the result.

Function#compose 文档:

Returns a composed function that first applies the before function to its input, and then applies this function to the result.

所以在 Funtion#andThen 的情况下,编译器已经可以使用参数的类型推断,而在第二种情况下(即 Function#compose)则不是。

顺便说一句,我更喜欢这样使用compose

import java.util.function.Function;

public class Main {
    public static void main(String[] args) {
        Function<String, String> test = (s) -> s.concat("foo");
        Function<String, String> upper = String::toUpperCase;    
        String str = test.compose(upper).apply("bar");
        //...
    }
}

让我们将其分解为尝试(粗略的)探索编译器为何发出错误。

1。 andThencompose 在这个上下文中的区别

下面是两种方法的签名(略default):

<V> Function<T, V> andThen(Function<? super R, ? extends V> after)
<V> Function<V, R> compose(Function<? super V, ? extends T> before)

记住 Function 是用两个类型变量 <T, R> 声明的,在这种情况下 afterbefore 之间最显着的区别是 after 应该接受数据类型为 R 的参数,当前函数的 return 类型,而 before 的参数的数据类型是 compose 的本地类型类型变量 V,编译器 必须推断 .

为什么这很重要?

2。为什么 test.andThen(String::toUpperCase) 有效?

在编译器试图做的许多事情中,它试图验证 String::toUpperCaseandThencompose 的有效参数。为此,它需要确定 String::toUpperCase 的类型,这意味着它需要推断该调用上下文中方法引用的数据类型。好吧,编译器知道 String::toUpperCase 必须是 Function,所以它只需要推断 Function 的类型参数,这就是上面的区别所在:

  • afterandThen的参数)的情况下,Function<? super R, ? extends V>的第一个参数(大致)已知为R,它有与当前函数的 (test's) return 类型相同。第二个类型参数 V 也对应于 andThen 的局部类型变量,被推断为方法引用将声明为 return.
  • 的任何内容
  • beforecompose的参数)的情况下,第一个参数的类型是未知的,必须根据上下文推断。对应compose的类型变量V。这就是第一个不同之处。

在 Eclipse 中,编译器错误是 The type String does not define toUpperCase(Object) that is applicable here,这表明该错误与匹配 String#toUpperCase 的参数类型有关。
现在,如果您看到上面的内容:

  • after 是一个带有 String 参数的 Function,这使得编译器在某种程度上可以简单地解析 String::toUpperCaseString 方法正在瞄准。这本身就是一个庞大的话题,但它归结为编译器选择 link 大致作为 lambda 表达式 (String s) -> s.toUpperCase() 的函数。方法引用可以解析为这样的实例方法,其中函数的第一个参数成为方法调用的目标(在 Function<T, U> 的情况下没有参数)。
  • before 可以用相同的方式解决,除了编译器不确定 Function 的第一个类型参数是什么类型。而且因为 String#toUpperCase 被重载了,编译器不能决定将它推断为 (String s) -> s.toUpperCase(),因为 V 本来可以是一种数据类型,会强制它解析对重载的方法引用,String.toUpperCase(Locale)。换句话说,编译器正在处理先有鸡还是先有蛋的情况(它需要知道参数类型 V 来决定将哪个 String#toUpperCase 方法转换为 link,但可以不要使用 String#toUpperCase 签名来推断 V,因为有两种可能性使其不明确)。

3。你是怎么解决的

您通过将 String::toUpperCase 转换为 Function<String, String> 来编译此代码。那做了什么?它只是让编译器摆脱了上述困境:你告诉编译器 VString,而不是 Locale 或任何其他可能性,这导致编译器将方法引用解析为 (String s) -> s.toUpperCase()(没有空间让 String.toUpperCase(Locale) 成为有效选项)

4。其他解决方法

您可以通过为 beforeV:

强制显式类型参数来有效地做同样的事情
  • test.compose((String s) -> s.toUpperCase()).apply("bar"); 将编译,因为 (String s) 显式键入 before 的参数
  • 的数据类型
  • test.<String>compose(String::toUpperCase).apply("bar"); 通过帮助编译器的推理逻辑来做同样的事情,告诉它 V 是一个 String,这避免了提到的先有鸡还是先有蛋的情况以上。

这种类型的解决方案并不是什么奇怪的方法,您实际上是在做与 System.out.println((String)null) 中的强制转换在 System.out.println(null) 被编译器拒绝时所做的相同的事情,尽管这些 Function 方法包括泛型作为香料。