为什么此类型推断不适用于此 Lambda 表达式方案?

2022-09-01 04:36:55

我有一个奇怪的场景,其中类型推断在使用lambda表达式时并不像我预期的那样工作。以下是我真实场景的近似值:

static class Value<T> {
}

@FunctionalInterface
interface Bar<T> {
  T apply(Value<T> value); // Change here resolves error
}

static class Foo {
  public static <T> T foo(Bar<T> callback) {
  }
}

void test() {
  Foo.foo(value -> true).booleanValue(); // Compile error here
}

我在倒数第二行得到的编译错误是

方法布尔值() 未为对象类型定义

如果我将 lambda 转换为 :Bar<Boolean>

Foo.foo((Bar<Boolean>)value -> true).booleanValue();

或者,如果我更改 的方法签名以使用原始类型:Bar.apply

T apply(Value value);

然后问题就消失了。我期望它的工作方式是:

  • Foo.foo调用应推断返回类型boolean
  • value在 lambda 中应推断为 。Value<Boolean>

为什么此推理不能按预期工作,如何更改此 API 以使其按预期工作?


答案 1

引擎盖下

使用一些隐藏的功能,我们可以获得有关正在发生的事情的更多信息:javac

$ javac -XDverboseResolution=deferred-inference,success,applicable LambdaInference.java 
LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo(value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo(value -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Object>)Object
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: error: cannot find symbol
    Foo.foo(value -> true).booleanValue(); // Compile error here
                          ^
  symbol:   method booleanValue()
  location: class Object
1 error

这是很多信息,让我们分解一下。

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo(value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)

阶段:方法适用性阶段
实际情况:在类型参数中
传递的实际参数:显式类型参数
候选:可能适用的方法

actuals 是因为我们的隐式类型 lambda 与适用性无关。<none>

编译器会将 您对 的调用解析为 中唯一命名的方法。它已被部分实例化为(因为没有实际值或类型参数),但这可以在延迟推理阶段更改。foofooFooFoo.<Object> foo

LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo(value -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Object>)Object
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)

实例化签名:完全实例化的签名。这是此步骤的结果(此时不会对 的签名进行更多的类型推断)。
目标类型:进行调用的上下文。如果方法调用是赋值的一部分,它将是左侧。如果方法调用本身是方法调用的一部分,则它将是参数类型。foofoo

由于您的方法调用是悬空的,因此没有目标类型。由于没有目标类型,因此无法进行更多的推断,并且推断为 。fooTObject


分析

编译器在推理期间不使用隐式类型的 lambda。在某种程度上,这是有道理的。通常,给定 ,在拥有 的类型之前,您将无法编译。如果您确实尝试推断 from 的类型,则可能会导致先有鸡还是先有蛋的类型问题。在Java的未来版本中可能会对此进行一些改进。param -> BODYBODYparamparamBODY


解决 方案

Foo.<Boolean> foo(value -> true)

此解决方案提供了一个显式类型参数(请注意以下部分)。这会将方法签名的部分实例化更改为 ,这是您想要的。foowith type-args(Bar<Boolean>)Boolean

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.<Boolean> foo(value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: Boolean
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Boolean>)Boolean)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.<Boolean> foo(value -> true).booleanValue(); // Compile error here
                                    ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

Foo.foo((Value<Boolean> value) -> true)

此解决方案显式键入您的 lambda,使其与适用性相关(注意如下)。这会将方法签名的部分实例化更改为 ,这是您想要的。with actuals(Bar<Boolean>)Boolean

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: Bar<Boolean>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Boolean>)Boolean)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.foo((Value<Boolean> value) -> true).booleanValue(); // Compile error here
                                           ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

Foo.foo((Bar<Boolean>) value -> true)

同上,但味道略有不同。

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
       ^
  phase: BASIC
  with actuals: Bar<Boolean>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Boolean>)Boolean)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
           ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.foo((Bar<Boolean>) value -> true).booleanValue(); // Compile error here
                                         ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

Boolean b = Foo.foo(value -> true)

此解决方案为方法调用提供显式目标(见下文)。这允许延迟实例化推断类型参数应为而不是(见下文)。target-typeBooleanObjectinstantiated signature

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Boolean b = Foo.foo(value -> true);
                   ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Boolean b = Foo.foo(value -> true);
                       ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: Boolean
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)

免責聲明

这是正在发生的行为。我不知道这是否是JLS中指定的内容。我可以四处挖掘,看看我是否可以找到指定此行为的确切部分,但是类型推断符号让我头疼。

这也不能完全解释为什么更改为使用 raw 可以解决此问题:BarValue

LambdaInference.java:16: Note: resolving method foo in type Foo to candidate 0
    Foo.foo(value -> true).booleanValue();
       ^
  phase: BASIC
  with actuals: <none>
  with type-args: no arguments
  candidates:
      #0 applicable method found: <T>foo(Bar<T>)
        (partially instantiated to: (Bar<Object>)Object)
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: Deferred instantiation of method <T>foo(Bar<T>)
    Foo.foo(value -> true).booleanValue();
           ^
  instantiated signature: (Bar<Boolean>)Boolean
  target-type: <none>
  where T is a type-variable:
    T extends Object declared in method <T>foo(Bar<T>)
LambdaInference.java:16: Note: resolving method booleanValue in type Boolean to candidate 0
    Foo.foo(value -> true).booleanValue();
                          ^
  phase: BASIC
  with actuals: no arguments
  with type-args: no arguments
  candidates:
      #0 applicable method found: booleanValue()

出于某种原因,将其更改为使用 raw 允许延迟实例化推断为 。如果我必须推测,我会猜测当编译器试图将lambda拟合到时,它可以通过查看lambda的主体来推断。这意味着我之前的分析是不正确的。编译器可以对 lambda 的主体执行类型推断,但只能对出现在返回类型中的类型变量执行类型推断。ValueTBooleanBar<T>TBoolean


答案 2

对 lambda 参数类型的推理不能依赖于 lambda 主体。

编译器面临着一项艰巨的工作,试图理解隐式 lambda 表达式

    foo( value -> GIBBERISH )

在编译胡言乱语之前,必须首先推断出 的类型,因为一般来说,胡言乱语的解释取决于 的定义。valuevalue

(在你的特殊情况下,胡言乱语恰好是一个与无关的简单常量。

Javac 必须首先推断参数 ;因此,上下文中没有约束。然后,lambda 主体被编译并识别为布尔值,与 兼容。Value<T>valueT=ObjecttrueT

对功能接口进行更改后,lambda 参数类型不需要推理;T 仍未推断。接下来,编译 lambda 主体,并且返回类型显示为布尔值,该值设置为 的下限。T


另一个演示该问题的示例

<T> void foo(T v, Function<T,T> f) { ... }

foo("", v->42);  // Error. why can't javac infer T=Object ?

T 被推断为 ;lambda的主体没有参与推理。String

在这个例子中,javac的行为对我们来说似乎非常合理;它可能阻止了编程错误。你不希望推理太强大;如果我们编写的所有内容都以某种方式编译,我们将失去编译器为我们查找错误的信心。


还有其他一些示例,其中 lambda 主体似乎提供了明确的约束,但编译器无法使用该信息。在 Java 中,必须先固定 lambda 参数类型,然后才能查看正文。这是一个深思熟虑的决定。相比之下,C#愿意尝试不同的参数类型,看看哪个使代码编译。Java认为这太冒险了。

在任何情况下,当隐式 lambda 失败时(这种情况经常发生),请为 lambda 参数提供显式类型;在你的情况下,(Value<Boolean> value)->true


推荐