ラムダ式を使用するときに、型推論が期待どおりに機能しないという奇妙なシナリオがあります。これが私の実際のシナリオの近似です:
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
を推測する必要があります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>)
_
フェーズ: メソッドの適用フェーズ
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
で推論を行うことはできず、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)
このソリューションは、ラムダを明示的に型指定します。これにより、適用性に関係することができます(以下の_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
を使用するように変更すると、据え置きインスタンス化でT
がBoolean
であると推測できます。推測する必要がある場合、コンパイラがラムダを_Bar<T>
_に適合させようとすると、本体の本文を見てT
がBoolean
であると推測できると思いますラムダ。これは、以前の分析が正しくないことを意味します。コンパイラはcanラムダの本体で型推論を実行しますが、onlyが戻り値の型で表示されます。
ラムダパラメータタイプの推論は、ラムダ本体に依存できません。
コンパイラーは暗黙のラムダ式を理解しようとする困難な仕事に直面しています
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
これを修正する簡単な方法は、foo
へのメソッド呼び出しでの型宣言です。
Foo.<Boolean>foo(value -> true).booleanValue();
編集:これが必要な理由に関する特定のドキュメントが、他の皆と同じように見つかりません。私はそれがプリミティブ型のせいかもしれないと思ったが、それは正しくなかった。とにかく、この構文は Target Type を使用して呼び出されます。また ラムダのターゲットタイプ 。理由はわかりませんが、この特定の使用例が必要な理由に関するドキュメントはどこにもありません。
編集2:私はこの関連する質問を見つけました:
ここでメソッドをチェーンしているからです。そこで受け入れられた回答で参照されているJSRコメントによると、コンパイラーには双方向のチェーンされたメソッド呼び出し間で推論されたジェネリック型情報を渡す方法がないため、機能の意図的な省略でした。その結果、booleanValue
の呼び出しに到達するまでに消去されたタイプ全体が消去されます。ターゲットタイプを追加すると、コンパイラーに JLS§18 で概説されている規則を使用して決定を行わせる代わりに、制約を手動で指定することでこの動作が削除されます。これは私が思いつくことができる唯一の情報です。誰かがより良い何かを見つけた場合、私はそれを見てみたいです。
他の回答と同様に、賢い誰かが指摘できることを願っていますwhyコンパイラがT
がBoolean
。
コンパイラーが既存のクラス/インターフェース設計に変更を加えることなく、正しいことを実行できるようにする1つの方法は、ラムダ式で仮パラメーターの型を明示的に宣言することです。したがって、この場合は、value
パラメータのタイプがValue<Boolean>
であることを明示的に宣言することによって。
void test() {
Foo.foo((Value<Boolean> value) -> true).booleanValue();
}
ラムダを誤って解釈したため、値は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;
}
}
public class UsingClass<T> {
public T foo(Callback<Value<T>, T> callback, Value<T> value) {
return callback.call(value);
}
}
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);
}
}
理由はわかりませんが、戻り値の型を個別に追加する必要があります。
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();
}
}
一部の賢い人はおそらくそれを説明できます。