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

Why is this type inference not working with this Lambda expression scenario?

我有一个奇怪的场景,在使用 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
}

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

The method booleanValue() is undefined for the type Object

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

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

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

T apply(Value value);

然后问题就解决了。我希望它起作用的方式是:

为什么这个推理没有按预期工作?我如何更改这个 API 以使其按预期工作?

我不知道为什么,但您需要添加单独的 return 类型:

public class HelloWorld{
static class Value<T> {
}

@FunctionalInterface
interface Bar<T,R> {
      R apply(Value<T> value); // Return type added
}

static class Foo {
  public static <T,R> R foo(Bar<T,R> callback) {
      return callback.apply(new Value<T>());
  }
}

void test() {
  System.out.println( Foo.foo(value -> true).booleanValue() ); // No compile error here
}
     public static void main(String []args){
         new HelloWorld().test();
     }
}

聪明的人可能会解释。

像其他答案一样,我也希望更聪明的人可以指出 为什么 编译器无法推断出 TBoolean.

无需对现有 class/interface 设计进行任何更改即可帮助编译器做正确事情的一种方法是在 lambda 表达式中显式声明形式参数的类型。因此,在这种情况下,通过显式声明 value 参数的类型是 Value<Boolean>.

void test() {
  Foo.foo((Value<Boolean> value) -> true).booleanValue();
}

解决此问题的简单方法是在对 foo 的方法调用上进行类型声明:

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

编辑: 我找不到关于为什么这是必要的具体文档,就像其他人一样。我怀疑这可能是因为原始类型,但那是不对的。无论如何,使用 Target Type. Also Target Type in Lambdas 调用此语法。虽然原因让我难以理解,但我无法在任何地方找到关于为什么需要这个特定用例的文档。

编辑 2: 我发现了这个相关问题:

Generic type inference not working with method chaining?

看起来是因为您在此处链接方法。根据那里接受的答案中引用的 JSR 评论,这是故意遗漏的功能,因为编译器没有办法在两个方向的链式方法调用之间传递推断的泛型类型信息。结果,整个类型在调用 booleanValue 时被擦除。添加目标类型通过手动提供约束而不是让编译器使用 JLS §18 中概述的规则做出决定来消除此行为,JLS §18 似乎根本没有提到这一点。这是我唯一能想到的信息。如果有人找到更好的东西,我很乐意看到它。

问题

Value 将推断为 Value<Object> 类型,因为您对 lambda 的解释有误。想一想,就像您直接使用 lambda 调用 apply 方法一样。所以你要做的是:

Boolean apply(Value value);

这被正确地推断为:

Boolean apply(Value<Object> value);

因为您还没有给出值的类型。

简单的解决方案

以正确的方式调用 lambda:

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

这将被推断为:

Boolean apply(Value<Boolean> value);

(我的)推荐方案

你的解决方案应该更清楚一点。如果你想要一个回调,那么你需要一个将被返回的类型值。

我制作了一个通用回调接口、一个通用值 class 和一个 UsingClass 来展示如何使用它。

回调接口

/**
 *
 * @param <P> The parameter to call
 * @param <R> The return value you get
 */
@FunctionalInterface
public interface Callback<P, R> {

  public R call(P param);
}

价值class

public class Value<T> {

  private final T field;

  public Value(T field) {
    this.field = field;
  }

  public T getField() {
    return field;
  }
}

UsingClassclass

public class UsingClass<T> {

  public T foo(Callback<Value<T>, T> callback, Value<T> value) {
    return callback.call(value);
  }
}

主测试应用程序

public class TestApp {

  public static void main(String[] args) {
    Value<Boolean> boolVal = new Value<>(false);
    Value<String> stringVal = new Value<>("false");

    Callback<Value<Boolean>, Boolean> boolCb = (v) -> v.getField();
    Callback<Value<String>, String> stringCb = (v) -> v.getField();

    UsingClass<Boolean> usingClass = new UsingClass<>();
    boolean val = usingClass.foo(boolCb, boolVal);
    System.out.println("Boolean value: " + val);

    UsingClass<String> usingClass1 = new UsingClass<>();
    String val1 = usingClass1.foo(stringCb, stringVal);
    System.out.println("String value: " + val1);

    // this will give you a clear and understandable compiler error
    //boolean val = usingClass.foo(boolCb, stringVal);
  }
}

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

编译器在试图理解隐式 lambda 表达式时面临着艰巨的任务

    foo( value -> GIBBERISH )

必须先推断value的类型,然后才能编译GIBBERISH,因为一般来说,对GIBBERISH的解释取决于value的定义。

(在您的特殊情况下,GIBBERISH 恰好是一个独立于 value 的简单常量。)

Javac 必须首先为参数 value 推断 Value<T>;上下文中没有约束,因此 T=Object。然后,lambda body true 被编译并识别为布尔值,与 T.

兼容

在您对功能接口进行更改后,lambda 参数类型不需要推断; T 仍未推断。接下来,编译 lambda 主体,return 类型显示为布尔值,它被设置为 T 的下限。


另一个演示问题的例子

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

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

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

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


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

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

引擎盖下

使用一些隐藏的 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>)

相位:method applicability phase
actuals:传入的实际参数
类型参数:显式类型参数
候选人:potentially applicable methods

actuals 是 <none> 因为我们隐式类型的 lambda 不是 pertinent to applicability.

编译器将您对 foo 的调用解析为 Foo 中唯一名为 foo 的方法。它已被部分实例化为 Foo.<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>)

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

由于您的方法调用是悬空的,因此没有目标类型。由于没有target-type,无法对foo做更多的推断,T被推断为Object.


分析

编译器在推理过程中不使用隐式类型的 lambda。在某种程度上,这是有道理的。通常,给定 param -> BODY,您将无法编译 BODY,直到您拥有 param 的类型。如果您确实尝试从 BODY 推断 param 的类型,则可能会导致先有鸡还是先有蛋的类型问题。在 Java.

的未来版本中可能会对此进行一些改进

解决方案

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

此解决方案为 foo 提供了一个显式类型参数(请注意下面的 with 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-type)。这允许延迟实例化推断类型参数应该是 Boolean 而不是 Object(请参阅下面的 instantiated 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 中指定的内容。我可以四处挖掘,看看是否可以找到指定此行为的确切部分,但 type inference 符号让我头疼。

这也不能完全解释为什么将 Bar 更改为使用原始 Value 会解决此问题:

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

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