繁体   English   中英

为什么此类型推断不适用于此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
}

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

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

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

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

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

T apply(Value value);

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

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

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

在引擎盖下

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

阶段: 方法适用阶段
实际值:传入的实际参数
type-args:显式类型参数
候选人: 可能适用的方法

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

编译器将您对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进行更多推断,并且推断TObject


分析

编译器在推理期间不使用隐式类型的lambdas。 在某种程度上,这是有道理的。 一般来说,给定param -> BODY ,在你有一个param类型之前,你将无法编译BODY 如果你确实试图从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中指定的内容。 我可以四处搜索,看看我是否能找到指定此行为的确切部分,但类型推断符号让我头疼。

这也无法完全解释为什么更改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的主体执行类型推断,但仅限于出现在返回类型中的类型变量。

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

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

    foo( value -> GIBBERISH )

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

(在你的特殊情况下,GIBBERISH恰好是一个与value无关的简单常数。)

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

在对功能接口进行更改后,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被推断为String ; lambda的身体没有参与推论。

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


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

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

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

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

编辑:我找不到有关为什么这是必要的具体文档,几乎就像其他人一样。 我怀疑它可能是因为原始类型,但那是不对的。 无论如何,使用目标类型调用此语法。 也是Lambda中的目标类型 原因让我不知所措,我找不到任何关于为什么这个特殊用例是必要的文档。

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

通用类型推断无法使用方法链接?

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

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

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

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

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

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

一些聪明的家伙可能会解释一下。

问题

值将推断为类型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类

public class UsingClass<T> {

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM