为什么 compose() 需要显式强制转换而 Then() 不需要?

2022-09-03 07:17:46

我正在研究功能组成,并有一个例子:

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::toUpperCaseFunction <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()


答案 1

正如错误消息所示,问题与具有接受a作为参数的重载的事实有关,因此编译器错误是由不知道应该引用哪个重载引起的。原则上,编译器应该能够知道具有两个参数的方法引用(对象本身是第一个参数)不能是 ,但我认为没有这方面的规则 - 或者更确切地说,在泛型类型参数的推理过程中似乎没有规则,这就是为什么显式转换解决了这个问题。toUpperCaseLocaleString::toUpperCaseStringFunction

我们可以通过尝试不同的方法引用(如 String::trim)来确认重载确实是导致问题的原因,该方法引用没有重载:

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

所以现在的问题是,为什么重载在用 .不同之处在于,这两个函数以相反的顺序组成,因此需要推断的类型参数是 的输出类型,这与选择哪个重载无关。因此,当方法引用的返回类型将用于推理时,编译器似乎有一个规则来处理类型参数推理期间重载的模糊性,并且重载具有相同的返回类型。请注意,对于具有相同参数类型的重载,不能存在类似的规则,因为重载不能具有相同的参数类型。andThenandThenString::toUpperCaseString


答案 2

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

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>afterbeforeafterRbeforecomposeV

为什么这与此相关?

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

在编译器尝试执行的许多操作中,它试图验证这是 和 的有效参数。为此,它需要建立 的类型,这意味着它需要在该调用上下文中推断方法引用的数据类型。好吧,编译器知道 它必须是 一个 ,所以它只需要推断 的类型参数,这就是上述差异适用的地方:String::toUpperCaseandThencomposeString::toUpperCaseString::toUpperCaseFunctionFunction

  • 在 (参数 的 参数 ) 的情况下,的第一个参数已知是 (粗略地),它必须与当前函数的 ('s) 返回类型相同。第二个类型参数 ,也对应于 的局部类型变量,被推断为将声明返回的方法引用。afterandThenFunction<? super R, ? extends V>RtestVandThen
  • 在 (参数 ) 的情况下,第一个参数的类型是未知的,必须在上下文中推断。它对应于 的类型变量 。这是第一个区别所在。beforecomposecomposeV

在 Eclipse 中,编译器错误为 ,这给出了错误与 匹配 的参数类型相关的线索。
现在,如果您看到上面的内容:The type String does not define toUpperCase(Object) that is applicable hereString#toUpperCase

  • after是一个采用参数的函数,这使得编译器解析目标方法变得有些简单。这本身就是一个巨大的主题,但它归结为编译器选择将函数大致链接为lambda表达式 。方法引用可以解析为这样的实例方法,其中函数的第一个参数成为方法调用的目标(在 的情况下没有参数)。FunctionStringStringString::toUpperCase(String s) -> s.toUpperCase()Function<T, U>
  • before可以以相同的方式解决,除了编译器不确定的第一个类型参数的类型是什么。而且由于重载,编译器不能只是决定将其推断为 ,因为可能意味着数据类型将强制它解析对重载的方法引用。换句话说,编译器正在处理先有鸡还是先有蛋的情况(它需要知道参数类型来决定链接哪种方法,但不能使用签名来推断,因为有两种可能性使它变得模糊)。FunctionString#toUpperCase(String s) -> s.toUpperCase()VString.toUpperCase(Locale)VString#toUpperCaseString#toUpperCaseV

3. 您如何解决

您通过强制转换为 来编译此代码。这是做什么的?它只是让编译器摆脱了上述困境:你告诉编译器这是,不是或任何其他可能性,这导致编译器将方法引用解析为(没有留下任何空间成为有效的选项)String::toUpperCaseFunction<String, String>VStringLocale(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");通过帮助编译器的推理逻辑来做同样的事情,告诉它是一个 ,这避免了上面提到的先有鸡还是先有蛋的情况。VString

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


推荐