38

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

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生の型を使うには:

T apply(Value value);

それから問題は消えます。これが機能すると期待する方法は、次のとおりです。

  • Foo.foo呼び出しはの戻り型を推測する必要がありますboolean
  • valueラムダではValue<Boolean>

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


  • どのJavaバージョンを使用しますか? - NwDev
  • @NwDx Java 1.8.0_25 - Josh Stone
  • BooleanはValue< Boolean>ではないため、単に型が一致しません。 Tが値であるべきであることをコンパイラがどのように知ることができるべきか< T>?そしてもちろんそれがどうやってわからないのですか、それはオブジェクトを想定しています。を見て関数インタフェース適用方法値は、Bar Interfaceのコンテキストにおける具体的な型です。 - NwDev
  • @NwDxあなたが説明している内容が、以下の@ fukanchikの回答の基本だと思います。問題は、コンパイラが推測することです。Value<Object>の代わりにValue<Boolean>これは私が後にしているものです。それを機能させる方法についてのアイデアを共有すること自由に感じなさい。 - Josh Stone
  • Foo.fooの戻り型を捨てていないのであれば、コンパイラはターゲット型を使ってTの境界を導き出すことができます。 - Brian Goetz

6 답변


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

段階:メソッド適用フェーズ

actuals:渡された実際の引数

type-args:明示的な型引数

候補者:適用可能性がある方法

現在は<none>暗黙的に型指定されたラムダは適用性に関連する

コンパイラは、の呼び出しを解決します。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>)

インスタンス化された署名:の完全にインスタンス化された署名foo。これはこのステップの結果です(この時点での署名についてこれ以上の型推論は行われません)foo

target-type:呼び出しが行われているコンテキスト。メソッド呼び出しが代入の一部である場合は、左側になります。メソッド呼び出し自体がメソッド呼び出しの一部である場合、それはパラメータ型になります。

あなたのメソッド呼び出しはぶら下がっているので、target-typeはありません。 target-typeがないため、これ以上推論を行うことはできません。fooそしてTあると推定されるObject


分析

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

この解決法はあなたのラムダを明示的にタイプします、それはそれが適用性に適切であることを可能にします(notewith 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で指定されているものかどうかはわかりません。この振る舞いを指定した正確なセクションを見つけることができるかどうかを調べてみることができますが、型推論表記は私に頭痛を与えます。

これもまたなぜ変化するのかを完全には説明していない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遅延インスタンス化がそれを推論できるようにします。TですBoolean。私が推測しなければならないならば、私はコンパイラーがλにλを当てはめようとする時にそれを推測するでしょうBar<T>、それはそれを推測することができますTですBooleanラムダの体を見て。これは私の以前の分析が間違っていることを意味します。コンパイラできるラムダの本体に対して型推論を実行しますが、それはのみ戻り型に表示されます。


5

ラムダパラメータタイプの推論は、ラムダボディに依存することはできません。

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

    foo( value -> GIBBERISH )

の種類valueGIBBERISHのコンパイルは、GIBBERISHのコンパイル前に最初に推論されなければなりません。value

(あなたの特別なケースでは、GIBBERISHは偶然に関係なく単純な定数です。value。)

Javacは推測しなければならないValue<T>最初のパラメータvalue;文脈に制約はありませんので、T=Object。それから、ラムダボディtrueとコンパイルされ、Booleanとして認識されます。T

機能インターフェースを変更した後は、lambdaパラメーター・タイプは推論を必要としません。 Tは邪魔されないままでいる。次に、ラムダ本体がコンパイルされ、戻り値の型はBooleanのように見えます。これは、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


4

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

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

編集するこれがなぜ必要なのかについての具体的な文書を見つけることができません。私はそれがプリミティブ型のせいかもしれないと思ったが、それは正しくなかった。とにかく、この構文はaを使って呼ばれます。対象タイプ。またラムダのターゲットタイプ。理由は私を避けます、私はこの特定の使用例がなぜ必要であるかについての文書を見つけることができません。

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

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

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


  • JLS参照が必要かどうかわかりません - ここで推論がうまくいかないようで、APIに変更を加えることができるのであれば、一般的に不思議に思うだけです。それを働かせなさい。理想的には、ユーザーが手動でtype引数をここで指定する必要がないようにする必要はありません。 - Josh Stone
  • アルゴリズムとプリミティブを使って型推論を行う方法がすべてです。真の代わりにBoolean.TRUEを返してみましたか?私はIDEの前にいないので、現時点ではテストできません。 - Brian
  • StringやBoolean.TRUEのようなオブジェクトを返すと、同じ種類の問題が発生します。 - Josh Stone
  • 連鎖方法に問題がある場合は、署名をに変更することは期待できません。T apply(Value value);何らかのプラスの効果がありますが、それでもそうです。 - sstan
  • その構文はターゲットタイピングとは呼ばれません。ターゲットタイピングはBoolean b = Foo.foo(value -> true);あなたが記述する構文(Foo.<Boolean> foo(value->true))として知られている証人をタイプする。 - Jeffrey

2

他の答えと同様に、私は賢い人が指摘できることを願っていますなぜコンパイラはそれを推測できませんTですBoolean

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

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


  • クラス/インタフェースの設計を少し変更することができます。Foo.foo呼び出しは戻り値の型をブール値にします。valueラムダではValue<Boolean>。これは公開APIの一部であり、キャストを要求するとユーザビリティの問題が発生するため、キャストしたくありません。 - Josh Stone
  • 明確にするために、私の例ではキャストを実行しません。型を推論するのではなく、単にラムダ式の仮パラメータ型について明示的なものです。しかし、はい、私はあなたがなぜこの場合に正しい型を推論できないのかを知りたいと思います。 - sstan

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

賢い人はおそらくそれを説明できるだろう。


  • それは私達をもう少し近づかせますが、valueラムダ式の中では、Value<Object>の代わりにValue<Boolean>これは、ラムダ式本体にもっと多くのコードが含まれているという私の実際のシナリオではまだ問題を引き起こすでしょう... - Josh Stone
  • よく分からない。私の推測です:あなたはTでメソッドを呼び出したい場合は、ワイルドカードを入力する必要がありますstatic class Value<T extends Boolean>, interface Bar<T extends Boolean,R>, public static <T extends Boolean,R> R foo(Bar<T,R> callback)など - fukanchik
  • もちろんBooleanをあなたのクラスに置き換えてください。 - fukanchik
  • ラムダからの戻り値の型が(ユーザーによって制御される)ものになる可能性があることを除けば、そのようなものでもうまくいくように思えます。ブール値はほんの一例です... - Josh Stone

1

問題

値は型を推測します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);
  }
}


  • この答えはシナリオ例から少し離れているように見えますが、これは実際のAPIを単純化したものです。 2つのこと:fooメソッドは静的である必要があり、単一の引数を取ります。 - Josh Stone
  • @JoshStoneあなたが話しているAPI。私がやったことはJavaFXでテーブルセル値ファクトリのためのコールバックに少し似ています。静的にしたいというのは、私が思う問題ではありません。しかし唯一の価値は?すみませんが、それからあなたはおそらく必要です述語関数やコールバックではありません。 - NwDev
  • もう一度言及する価値があります - これはパブリックAPIの一部になるので、使いやすくする必要があります。ユーザーは以下のような基本的なラムダを提供できる必要があります。value -> trueどの値が推論されるかValue<Boolean>そしてラムダを受け入れるメソッドは戻るBoolean。それ以外の変更は問題ありませんが、APIを使用可能にするためにはそのままにしておく必要があります。 - Josh Stone
  • うーん、私にはAPIは柔軟で抽象的であるべきです。あなたがBarでやったことは具体的なTypeを使っています、これはまったく柔軟ではありません。 - NwDev
  • 問題を説明するための例にすぎません。本物のAPIの目標は柔軟性ではなく、単純さであり、それが私たちが望む理由です。fooこれを機能させるために単一の引数を受け入れること。 - Josh Stone

リンクされた質問


関連する質問

最近の質問