web-dev-qa-db-ja.com

この型推論がこのラムダ式シナリオで機能しないのはなぜですか?

ラムダ式を使用するときに、型推論が期待どおりに機能しないという奇妙なシナリオがあります。これが私の実際のシナリオの近似です:

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
}

2行目から最後の行で発生するコンパイルエラーは

メソッドbooleanValue()はタイプObjectに対して未定義です

ラムダをBar<Boolean>にキャストした場合:

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

または、Bar.applyのメソッドシグネチャをrawタイプを使用するように変更した場合:

T apply(Value value);

その後、問題はなくなります。これが機能することを期待する方法は、次のとおりです。

  • Foo.fooの呼び出しは、戻り値の型booleanを推測する必要があります
  • ラムダのvalueValue<Boolean>に推論される必要があります。

この推論が期待どおりに機能しないのはなぜですか?このAPIを変更して期待どおりに機能させるにはどうすればよいですか?

38
Josh Stone

フードの下

いくつかの隠し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:渡される実際の引数
type-args:明示的な型引数
候補: 適用される可能性のある方法

実際の値は_<none>_です。これは、暗黙的に型指定されたラムダが 適用範囲に関連する ではないためです。

コンパイラーは、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であると推定されます。


分析

コンパイラーは、推論中に暗黙的に型指定されたラムダを使用しません。これはある程度、理にかなっています。一般に、_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)

このソリューションは、ラムダを明示的に型指定します。これにより、適用性に関係することができます(以下の_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_を参照)。これにより、据え置きインスタンス化で、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であると推測できます。推測する必要がある場合、コンパイラがラムダを_Bar<T>_に適合させようとすると、本体の本文を見てTBooleanであると推測できると思いますラムダ。これは、以前の分析が正しくないことを意味します。コンパイラはcanラムダの本体で型推論を実行しますが、onlyが戻り値の型で表示されます。

31
Jeffrey

ラムダパラメータタイプの推論は、ラムダ本体に依存できません。

コンパイラーは暗黙のラムダ式を理解しようとする困難な仕事に直面しています

    foo( value -> GIBBERISH )

一般にGIBBERISHの解釈はvalueの定義に依存するため、GIBBERISHをコンパイルする前に、valueのタイプを最初に推測する必要があります。

(特別なケースでは、GIBBERISHはたまたまvalueから独立した単純な定数です。)

Javacは最初にパラメータvalueに対してValue<T>を推測する必要があります。コンテキストには制約がないため、T=Object。次に、ラムダ本体trueがコンパイルされ、Tと互換性のあるブール値として認識されます。

関数インターフェイスに変更を加えた後は、ラムダパラメーターの型は推論を必要としません。 Tは据え置きのままです。次に、ラムダ本体がコンパイルされ、戻り値の型はブール値のように見えます。これはTの下限として設定されています。


問題を示す別の例

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

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

TはStringであると推定されます。ラムダの体は推論に参加しませんでした。

この例では、javacの動作は非常に合理的です。それはおそらくプログラミングエラーを防ぎました。推論が強力になりすぎないようにしてください。作成したすべてが何らかの形でコンパイルされると、コンパイラーがエラーを見つける自信がなくなります。


ラムダ本体が明確な制約を提供しているように見える他の例がありますが、コンパイラーはその情報を使用できません。 Javaでは、本体を確認する前に、ラムダパラメータタイプを最初に修正する必要があります。これは意図的な決定です。対照的に、C#はさまざまなパラメーター型を試して、コードがコンパイルされるかどうかを確認します。 Javaはリスクが高すぎると考えています。

いずれにせよ、かなり頻繁に発生する暗黙のラムダが失敗した場合は、ラムダパラメータに明示的な型を指定してください。あなたの場合、(Value<Boolean> value)->true

5
ZhongYu

これを修正する簡単な方法は、fooへのメソッド呼び出しでの型宣言です。

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

編集:これが必要な理由に関する特定のドキュメントが、他の皆と同じように見つかりません。私はそれがプリミティブ型のせいかもしれないと思ったが、それは正しくなかった。とにかく、この構文は Target Type を使用して呼び出されます。また ラムダのターゲットタイプ 。理由はわかりませんが、この特定の使用例が必要な理由に関するドキュメントはどこにもありません。

編集2:私はこの関連する質問を見つけました:

ジェネリック型の推論はメソッドチェーンでは機能しませんか?

ここでメソッドをチェーンしているからです。そこで受け入れられた回答で参照されているJSRコメントによると、コンパイラーには双方向のチェーンされたメソッド呼び出し間で推論されたジェネリック型情報を渡す方法がないため、機能の意図的な省略でした。その結果、booleanValueの呼び出しに到達するまでに消去されたタイプ全体が消去されます。ターゲットタイプを追加すると、コンパイラーに JLS§18 で概説されている規則を使用して決定を行わせる代わりに、制約を手動で指定することでこの動作が削除されます。これは私が思いつくことができる唯一の情報です。誰かがより良い何かを見つけた場合、私はそれを見てみたいです。

4
Brian

他の回答と同様に、賢い誰かが指摘できることを願っていますwhyコンパイラがTBoolean

コンパイラーが既存のクラス/インターフェース設計に変更を加えることなく、正しいことを実行できるようにする1つの方法は、ラムダ式で仮パラメーターの型を明示的に宣言することです。したがって、この場合は、valueパラメータのタイプがValue<Boolean>であることを明示的に宣言することによって。

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

問題

ラムダを誤って解釈したため、値はValue<Object>と推定されます。ラムダで直接applyメソッドを呼び出すように考えてください。だからあなたがすることは:

Boolean apply(Value value);

これは正しく次のように推測されます。

Boolean apply(Value<Object> value);

valueのタイプを指定していないためです。

シンプルなソリューション

正しい方法でラムダを呼び出します。

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

これは次のように推測されます:

Boolean apply(Value<Boolean> value);

(マイ)推奨ソリューション

ソリューションはもう少し明確にする必要があります。コールバックが必要な場合は、返されるtype値が必要です。

汎用的な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

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);
  }
}
1
aw-think

理由はわかりませんが、戻り値の型を個別に追加する必要があります。

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

一部の賢い人はおそらくそれを説明できます。

1
fukanchik