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

Jos*_*one 38 java lambda type-inference java-8

我有一个奇怪的场景,类型推断不起作用,因为我期望使用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
}
Run Code Online (Sandbox Code Playgroud)

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

对于Object类型,方法booleanValue()未定义

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

Foo.foo((Bar<Boolean>)value -> true).booleanValue();
Run Code Online (Sandbox Code Playgroud)

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

T apply(Value value);
Run Code Online (Sandbox Code Playgroud)

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

  • Foo.foo call应该推断出一个返回类型 boolean
  • value在lambda中应该推断出来Value<Boolean>.

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

Jef*_*rey 31

在引擎盖下

使用一些隐藏的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
Run Code Online (Sandbox Code Playgroud)

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

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>)
Run Code Online (Sandbox Code Playgroud)

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

实际值是<none>因为我们的隐式类型lambda 与适用性无关.

编译器解决您的调用foo来命名的唯一方法fooFoo.它已被部分实例化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>)
Run Code Online (Sandbox Code Playgroud)

实例化签名:完全实例化的签名foo.这是此步骤的结果(此时不再对签名进行类型推断foo).
target-type:正在进行调用的上下文.如果方法调用是赋值的一部分,则它将是左侧.如果方法调用本身是方法调用的一部分,那么它将是参数类型.

由于您的方法调用是悬空的,因此没有目标类型.由于没有目标类型,因此不能进行更多推断fooT推断出Object.


分析

编译器在推理期间不使用隐式类型的lambdas.在某种程度上,这是有道理的.一般来说,给定param -> BODY,BODY在你有一个类型之前你将无法编译param.如果您确实尝试推断出param来自的类型BODY,则可能导致鸡和蛋类型问题.在将来的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()
Run Code Online (Sandbox Code Playgroud)

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()
Run Code Online (Sandbox Code Playgroud)

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()
Run Code Online (Sandbox Code Playgroud)

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>)
Run Code Online (Sandbox Code Playgroud)

放弃

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

这也无法完全解释为什么更改Bar使用raw 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()
Run Code Online (Sandbox Code Playgroud)

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


Zho*_*gYu 5

对lambda参数类型的推断不能依赖于lambda体.

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

    foo( value -> GIBBERISH )
Run Code Online (Sandbox Code Playgroud)

value在编译GIBBERISH之前必须首先推断出类型,因为一般来说,GIBBERISH的解释取决于GIBBERISH的定义value.

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

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

在对功能接口进行更改后,lambda参数类型不需要推理; T仍未被提及.接下来,编译lambda主体,返回类型显示为Boolean,将其设置为下限T.


另一个证明问题的例子

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

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

T被推断为String; lambda的身体没有参与推论.

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


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

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