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

时间:2015-07-05 04:28:16

标签: java lambda java-8 type-inference

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

<script>
function hideButtons() {
$(".hideme").css('display', 'none');
}
</script>

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

  

方法booleanValue()未定义为Object

类型

如果我将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 }

Bar<Boolean>

或者如果我将Foo.foo((Bar<Boolean>)value -> true).booleanValue(); 的方法签名更改为使用原始类型:

Bar.apply
然后问题就消失了。我期望这种方式发挥作用的方式是:

  • T apply(Value value); 调用应推断返回类型Foo.foo
  • 应将lambda中的
  • boolean推断为value

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

6 个答案:

答案 0 :(得分:30)

引擎盖下

使用一些隐藏的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
实际值:传递的实际参数
type-args:显式类型参数
候选人:potentially applicable methods

实际值为<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。如果您确实尝试从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()

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的主体上执行类型推断,但仅限于在返回类型中出现的类型变量。

答案 1 :(得分:5)

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

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

    foo( value -> GIBBERISH )

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

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

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

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

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

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

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

答案 2 :(得分:4)

解决这个问题的简单方法是调用foo的方法的类型声明:

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

编辑:我无法找到有关其原因的具体文档,与其他人一样。我怀疑它可能是因为原始类型,但这是不对的。无论如何,使用Target Type调用此语法。 Target Type in Lambdas也是Generic type inference not working with method chaining?。原因让我不知所措,我无法在任何地方找到有关为什么需要这个特定用例的文档。

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

JLS §18

它看起来像是因为你在这里链接方法。根据在那里接受的答案中引用的JSR注释,它是故意省略功能,因为编译器没有办法在两个方向上的链式方法调用之间传递推断的泛型类型信息。结果,整个类型的时间被删除它到booleanValue的调用。添加目标类型可以通过手动提供约束来消除此行为,而不是让编译器使用{{3}}中列出的规则做出决定,这似乎根本没有提到这一点。这是我能想到的唯一信息。如果有人发现任何更好的东西,我很乐意看到它。

答案 3 :(得分:2)

与其他答案一样,我也希望更聪明的人可以指出 为什么 编译器无法推断T是{{1} }。

通过在lambda表达式中明确声明形式参数的类型,一种帮助编译器做正确的事情而不需要对现有类/接口设计进行任何更改的方法。因此,在这种情况下,通过明确声明Boolean参数的类型为value

Value<Boolean>

答案 4 :(得分:1)

我不知道为什么但你需要添加单独的返回类型:

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();
     }
}
一些聪明人可能会解释这一点。

答案 5 :(得分:1)

问题

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

Boolean apply(Value value);

这正确地推断为:

Boolean apply(Value<Object> value);

因为你还没有给出Value的类型。

简单解决方案

以正确的方式调用lambda:

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

这将被推断为:

Boolean apply(Value<Boolean> value);

(我的)推荐的解决方案

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

我已经制作了一个通用的Callback接口,一个通用的Value类和一个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);
}

价值等级

public class Value<T> {

  private final T field;

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

  public T getField() {
    return field;
  }
}

UsingClass class

public class UsingClass<T> {

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

TestApp with main

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);
  }
}