ラムダ式を使用したときに予想されるように、型推論が機能しないという奇妙なシナリオがあります。これが私の実際のシナリオの近似です。
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を変更して期待どおりに機能させるにはどうすればよいですか。
隠されたものを使う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:呼び出しが行われているコンテキスト。メソッド呼び出しが代入の一部である場合は、左側になります。メソッド呼び出し自体がメソッド呼び出しの一部である場合、それはパラメータ型になります。
あなたのメソッド呼び出しはぶら下がっているので、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
ラムダの体を見て。これは私の以前の分析が間違っていることを意味します。コンパイラできるラムダの本体に対して型推論を実行しますが、それはのみ戻り型に表示されます。
ラムダパラメータタイプの推論は、ラムダボディに依存することはできません。
暗黙のラムダ式を理解しようとすると、コンパイラは困難な仕事に直面します
foo( value -> GIBBERISH )
の種類value
GIBBERISHのコンパイルは、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
これを修正する簡単な方法は、メソッド呼び出しの型宣言です。foo
:
Foo.<Boolean>foo(value -> true).booleanValue();
編集するこれがなぜ必要なのかについての具体的な文書を見つけることができません。私はそれがプリミティブ型のせいかもしれないと思ったが、それは正しくなかった。とにかく、この構文はaを使って呼ばれます。対象タイプ。またラムダのターゲットタイプ。理由は私を避けます、私はこの特定の使用例がなぜ必要であるかについての文書を見つけることができません。
編集2:私はこの関連質問を見つけました:
ここでメソッドを連鎖しているからです。そこで受け入れられた回答で参照されているJSRのコメントによると、コンパイラは双方向のメソッド呼び出しの間で推測されたジェネリック型情報を渡す方法がないため、意図的な機能の省略です。結果として、それが呼び出しに到達するまでに消去されるの全種類booleanValue
。にターゲットタイプを追加すると、で説明した規則を使用してコンパイラに決定を行わせるのではなく、手動で制約を指定することで、この動作を排除できます。JLS第18条これについてはまったく言及していないようです。これが私が思い付くことができる唯一の情報です。誰かが何かより良いものを見つけたら、私はそれを見たいです。
T apply(Value value);
何らかのプラスの効果がありますが、それでもそうです。 - sstanBoolean b = Foo.foo(value -> true);
あなたが記述する構文(Foo.<Boolean> foo(value->true)
)として知られている証人をタイプする。 - Jeffrey
他の答えと同様に、私は賢い人が指摘できることを願っていますなぜコンパイラはそれを推測できませんT
ですBoolean
。
既存のクラス/インタフェース設計を変更することなく、コンパイラが正しいことを行えるようにする1つの方法は、ラムダ式で仮パラメータの型を明示的に宣言することです。したがって、この場合は、その型を明示的に宣言することによってvalue
パラメータはValue<Boolean>
。
void test() {
Foo.foo((Value<Boolean> value) -> true).booleanValue();
}
Foo.foo
呼び出しは戻り値の型をブール値にします。value
ラムダではValue<Boolean>
。これは公開APIの一部であり、キャストを要求するとユーザビリティの問題が発生するため、キャストしたくありません。 - Josh Stone
理由はわかりませんが、戻り値の型を別に追加する必要があります。
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 Stonestatic class Value<T extends Boolean>, interface Bar<T extends Boolean,R>, public static <T extends Boolean,R> R foo(Bar<T,R> callback)
など - fukanchik
値は型を推測します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);
}
}
value -> true
どの値が推論されるかValue<Boolean>
そしてラムダを受け入れるメソッドは戻るBoolean
。それ以外の変更は問題ありませんが、APIを使用可能にするためにはそのままにしておく必要があります。 - Josh Stonefoo
これを機能させるために単一の引数を受け入れること。 - Josh Stone
Value<Object>
の代わりにValue<Boolean>
これは私が後にしているものです。それを機能させる方法についてのアイデアを共有すること自由に感じなさい。 - Josh Stone