web-dev-qa-db-ja.com

なぜ消費者は、式の本体ではなく文の本体を持つラムダを受け入れるのですか?

驚くべきことに、次のコードは正常にコンパイルされています。

Consumer<String> p = ""::equals;

これも:

p = s -> "".equals(s);

しかし、これは期待どおりboolean cannot be converted to voidエラーで失敗します:

p = s -> true;

括弧を使用した2番目の例の変更も失敗します。

p = s -> ("".equals(s));

Javaコンパイラのバグですか、それとも私が知らない型推論規則がありますか?

61
Zefick

最初に、_Consumer<String>_が実際に何であるかを見る価値があります。 ドキュメントから

単一の入力引数を受け入れ、結果を返さない操作を表します。他のほとんどの機能的インターフェースとは異なり、Consumerは副作用を介して動作することが期待されています。

したがって、文字列を受け入れて何も返さない関数です。

_Consumer<String> p = ""::equals;
_

equalsは文字列(および実際には任意のオブジェクト)を取得できるため、正常にコンパイルされます。 equalsの結果は無視されます。*

_p = s -> "".equals(s);
_

これはまったく同じですが、構文が異なります。 returnは値を返すべきではないため、コンパイラは暗黙のConsumerを追加しないことを知っています。 wouldラムダが_Function<String, Boolean>_であった場合、暗黙のreturnを追加します。

_p = s -> true;
_

これは文字列(s)を取りますが、trueは式であり、ステートメントではないため、同じ方法で結果を無視することはできません。コンパイラhas toは、式が単独では存在できないため、暗黙的なreturnを追加します。したがって、このdoesには戻り値があります:ブール値。したがって、それはConsumerではありません。**

_p = s -> ("".equals(s));
_

繰り返しますが、これはexpressionであり、ステートメントではありません。ラムダをしばらく無視すると、System.out.println("Hello");という行が括弧で囲むと同様にコンパイルに失敗します。


* From 仕様

ラムダの本体がステートメント式(つまり、ステートメントとして単独で使用できる式)である場合、それはvoidを生成する関数型と互換性があります。結果はすべて破棄されます。

** 仕様 から(ありがとう、 ユージン ):

ラムダ式は、...ラムダ本体がステートメント式( §14.8 )またはvoid互換ブロックのいずれかである場合、[void-produce]関数タイプと一致します。

80
Michael

他の答えはラムダに焦点を当てることで説明を複雑にしていると思いますが、この場合のラムダの動作は手動で実装されたメソッドの動作に似ています。これはコンパイルします:

_new Consumer<String>() {
    @Override
    public void accept(final String s) {
        "".equals(s);
    }
}
_

一方、これはしません:

_new Consumer<String>() {
    @Override
    public void accept(final String s) {
        true;
    }
}
_

"".equals(s)はステートメントですが、trueはそうではないためです。 voidを返す関数型インターフェースのラムダ式にはステートメントが必要なので、メソッドの本体と同じルールに従います。

一般に、ラムダ本体は正確にメソッド本体と同じ規則に従いません-特に、本体が式であるラムダが値を返すメソッドを実装する場合、暗黙のreturn。したがって、たとえば_x -> true_は_Function<Object, Boolean>_の有効な実装になりますが、_true;_は有効なメソッド本体ではありません。しかし、この特定のケースでは、機能的インターフェースとメソッド本体は一致しています。

11
s -> "".equals(s)

そして

s -> true

同じ関数記述子に依存しないでください。

s -> "".equals(s)は、String->voidまたはString->boolean関数記述子のいずれかを参照できます。
s -> trueは、String->boolean関数記述子のみを参照します。

どうして ?

  • s -> "".equals(s)と書くと、ラムダの本体:"".equals(s)値を生成するステートメント
    コンパイラは、関数がvoidまたはbooleanを返す可能性があると見なします。

だから書く:

Function<String, Boolean> function = s -> "".equals(s);
Consumer<String> consumer = s -> "".equals(s);

有効です。

ラムダ本体をConsumer<String>宣言変数に割り当てると、記述子String->voidが使用されます。
もちろん、このコードはあまり意味がありません(等価性をチェックし、結果を使用しません)が、コンパイラーは気にしません。
ステートメントを書くときも同じです:myObject.getMyProperty() where getMyProperty()boolean値を返しますが、その結果を保存しません。

  • s -> trueと書くと、ラムダの本体:true単一の式.
    コンパイラーは、関数が必須booleanを返すと見なします。
    したがって、記述子String->booleanのみを使用できます。

さて、コンパイルされないコードに戻ってください。
あなたは何をしようとしているのですか ?

Consumer<String> p = s -> true;

できません。関数記述子Consumer<String>を使用する変数に、String->void関数記述子を持つラムダ本体を割り当てます。一致しません!

8
davidxxx