Java 8でラムダ構文の代わりにメソッド参照構文を使用すると、パフォーマンス上の利点はありますか?


56

メソッド参照はラムダラッパーのオーバーヘッドをスキップしますか?将来的にはそうなるのでしょうか?

メソッド参照に関するJavaチュートリアルによると:

時々...ラムダ式は既存のメソッドを呼び出すだけです。これらの場合、既存のメソッドを名前で参照する方が明確な場合がよくあります。メソッド参照により、これを行うことができます。これらは、既に名前を持っているメソッドのコンパクトで読みやすいラムダ式です。

いくつかの理由から、メソッド参照構文よりもラムダ構文の方が好きです。

ラムダはより明確です

Oracleの主張にもかかわらず、メソッド参照構文があいまいであるため、ラムダ構文はオブジェクトメソッド参照の速記より読みやすいと思います。

Bar::foo

xのクラスで静的な1引数のメソッドを呼び出してxを渡しますか?

x -> Bar.foo(x)

または、xで引数のないインスタンスメソッドを呼び出していますか?

x -> x.foo()

メソッド参照構文は、どちらの代わりにもなります。コードが実際に行っていることを隠します。

ラムダはより安全です

クラスメソッドとしてBar :: fooを参照し、Barが後で同じ名前のインスタンスメソッドを追加した場合(またはその逆)、コードはコンパイルされなくなります。

ラムダを一貫して使用できます

任意の関数をラムダでラップできます。そのため、どこでも同じ構文を一貫して使用できます。メソッド参照構文は、プリミティブ配列を取得または返すメソッド、チェックされた例外をスローするメソッド、またはインスタンスと静的メソッドとして使用されるメソッド名が同じメソッドでは機能しません(メソッド参照構文が呼び出されるメソッドについて曖昧だからです) 。同じ数の引数を持つメソッドをオーバーロードした場合は機能しませんが、とにかくそれを行うべきではないため(Josh Blochの項目41を参照)、メソッド参照に対してそれを保持することはできません。

結論

パフォーマンスの低下がない場合は、IDEで警告をオフにし、メソッド参照をコードに散在させることなくラムダ構文を一貫して使用したいと思います。

PS

ここでもそこでもありませんが、私の夢では、オブジェクトメソッドの参照は次のように見え、ラムダラッパーなしでメソッドに対してinvoke-dynamicを直接適用します。

_.foo()

2
これはあなたの質問には答えませんが、クロージャーを使用する他の理由があります。私の意見では、それらの多くは追加の関数呼び出しの小さなオーバーヘッドをはるかに上回っています。

9
パフォーマンスが時期尚早に心配されていると思います。私の意見では、パフォーマンスはメソッドリファレンスを使用するための2番目の考慮事項です。それはすべてあなたの意図を表現することです。関数をどこかに渡す必要がある場合、それを委任する他の関数の代わりに関数渡すだけではどうですか?私には奇妙に思えます。関数が一流のものだとは思わないように、それらを移動するには匿名のシャペロンが必要です。しかし、あなたの質問をより直接的に解決するために、メソッド参照を静的呼び出しとして実装できるのであれば驚かないでしょう。
ドーバル

7
.map(_.acceptValue()):間違っていなければ、これはScala構文に非常によく似ています。たぶん、あなたは間違った言語を使用しようとしているだけかもしれません。
ジョルジオ

2
「メソッド参照構文は、voidを返すメソッドでは機能しません」 -どうやって?私はいつも通り過ぎSystem.out::printlnforEach()います...?
ルーカスエダー

3
「メソッド参照構文は、プリミティブ配列を受け取ったり返したり、チェック済み例外をスローしたりするメソッドでは機能しません。...」それは正しくありません。そのような場合は、問題の機能インターフェイスで許可されている限り、メソッド参照とラムダ式を使用できます。
スチュアートマーク

回答:


12

多くのシナリオで、ラムダとメソッド参照は同等だと思います。ただし、ラムダは、宣言するインターフェイスタイプによって呼び出しターゲットをラップします。

例えば

public class InvokeTest {

    private static void invoke(final Runnable r) {
        r.run();
    }

    private static void target() {
        new Exception().printStackTrace();
    }

    @Test
    public void lambda() throws Exception {
        invoke(() -> target());
    }

    @Test
    public void methodReference() throws Exception {
        invoke(InvokeTest::target);
    }
}

コンソールからスタックトレースが出力されます。

ではlambda()、メソッド呼び出しtarget()はでlambda$lambda$0(InvokeTest.java:20)、追跡可能な行情報があります。明らかに、それはあなたが書いたラムダであり、コンパイラはあなたのために匿名メソッドを生成します。そして、ラムダメソッドの呼び出し元はのようなものInvokeTest$$Lambda$2/1617791695.run(Unknown Source)、つまりinvokedynamicJVMでの呼び出しです。つまり、呼び出しは生成されたメソッドにリンクされます。

ではmethodReference()、メソッド呼び出しtarget()は直接でありInvokeTest$$Lambda$1/758529971.run(Unknown Source)、呼び出しInvokeTest::targetメソッドに直接リンクされていることを意味します。

結論

とりわけ、ラムダ式を使用すると、ラムダからの生成メソッドへのメソッド呼び出しが1つだけ発生し ます。


1
メタファクトリーについての以下の答えはもう少し良いです。関連する有用なコメントをいくつか追加しました(Oracle / OpenJDKなどの実装がASMを使用して、ラムダ/メソッドハンドルインスタンスの作成に必要なラッパークラスオブジェクトを生成する方法
Ajax

29

それはすべてメタファクトリーです

まず、ほとんどのメソッド参照は、ラムダメタファクトリによるデシュガー化を必要とせず、単に参照メソッドとして使用されます。ラムダ式翻訳(「TLE」)記事の「ラムダ体糖化」セクションの下:

すべてのものが等しい、プライベートメソッドは非プライベート、静的メソッドはインスタンスメソッドよりも好ましいです。ラムダ式が表示される最も内側のクラスでラムダボディが脱糖されている場合が最適です、シグネチャはラムダのボディシグネチャと一致する必要があります、余分引数は、キャプチャされた値の引数リストの先頭に追加する必要があり、メソッド参照をまったくデ糖化しません。ただし、このベースライン戦略から逸脱する必要がある例外的なケースがあります。

これは、TLEの「The Lambda Metafactory」でさらに強調されています。

metaFactory(MethodHandles.Lookup caller, // provided by VM
            String invokedName,          // provided by VM
            MethodType invokedType,      // provided by VM
            MethodHandle descriptor,     // lambda descriptor
            MethodHandle impl)           // lambda body

impl引数はラムダ法、脱糖ラムダ本体またはメソッド参照で指定方法のいずれかを識別する。

静的(Integer::sum)または無制限のインスタンスメソッド(Integer::intValue)参照は、脱糖 なしで「高速パス」メタファクトリバリアントによって最適に処理できるという意味で、「最も簡単」または「最も便利」です。この利点は、TLEの「メタファクトリーバリアント」で有益に指摘されています。

不要な引数を削除することにより、クラスファイルは小さくなります。また、高速パスオプションにより、VMのバーが低くなり、ラムダ変換操作が本質的になり、「ボクシング」操作として扱うことができ、unboxの最適化が容易になります。

当然、インスタンスキャプチャメソッド参照(obj::myMethod)は、呼び出しのためのメソッドハンドルへの引数として境界付きインスタンスを提供する必要があります。これは、「ブリッジ」メソッドを使用したデシュガーの必要性を意味する場合があります。

結論

あなたが示唆しているラムダ「ラッパー」が何であるかは正確にはわかりませんが、ユーザー定義のラムダまたはメソッド参照を使用した最終的な結果は同じですが、到達する方法はまったく異なるようです現在そうでない場合は、将来異なる可能性があります。したがって、メソッド参照はメタファクトリーにより最適な方法で処理できる可能性が高いと思います。


1
これは、ラムダを作成するために必要な内部機構に実際に触れるため、最も関連する答えです。ラムダ作成プロセスを実際にデバッグした人(特定のラムダ/メソッド参照の安定したIDを生成してマップから削除できるようにする方法を理解するため)として、ラムダのインスタンス。Oracle / OpenJDKはASMを使用して、必要に応じてラムダ(またはメソッド参照のインスタンス修飾子)で参照される変数を閉じるクラスを動的に生成します...
Ajax

1
...変数のクローズに加えて、ラムダは特に宣言したクラスにインジェクトされる静的メソッドを作成するため、作成されたラムダは呼び出したい関数にマップするために呼び出すものを持っています。インスタンス修飾子を持つメソッド参照は、そのインスタンスをこのメソッドのパラメーターとして受け取ります。プライベートクラスのthis :: method参照がこのクロージャをスキップするかどうかはテストしていませんが、実際には、静的メソッドが中間メソッドをスキップすることをテストしました(そして、単に呼び出しターゲットに適合するオブジェクトを作成します)呼び出しサイト)。
アヤックス

1
おそらくプライベートメソッドも仮想ディスパッチを必要としませんが、よりパブリックなものは(生成された静的メソッドを介して)インスタンスを閉じる必要があるため、実際のインスタンスで仮想を呼び出してオーバーライドを確認できるようにする必要があります。TL; DR:メソッド参照は少ない引数で閉じられるため、リークが少なくなり、必要な動的コード生成が少なくなります。
アヤックス

3
最後のスパムコメント...動的なクラス生成はかなり重いです。クラスローダーに匿名クラスをロードするよりもおそらくおそらく優れています(最も重い部分は一度しか実行されないため)ラムダ対メソッド参照対匿名クラスの実際のベンチマークを見るのは興味深いでしょう。2つのラムダまたはメソッド参照は同じものを参照しますが、異なる場所で実行時に異なるクラスを作成することに注意してください(同じメソッド内でも、次々に)。
アヤックス


0

パフォーマンスに影響する可能性のあるラムダ式を使用すると、非常に深刻な結果が1つあります。

ラムダ式を宣言すると、ローカルスコープでクロージャーが作成れます。

これは何を意味し、パフォーマンスにどのように影響しますか?

まあ、あなたが尋ねてくれてうれしいです。これは、これらの小さなラムダ式のそれぞれが小さな匿名の内部クラスであり、ラムダ式の同じスコープ内にあるすべての変数への参照を保持することを意味します。

これはthis、オブジェクトインスタンスとそのすべてのフィールドの参照も意味します。ラムダの実際の呼び出しのタイミングによっては、ラムダを保持しているオブジェクトがまだ生きている限り、ガベージコレクターがこれらの参照を手放すことができないため、これはかなり重大なリソースリークになります。


非静的メソッド参照についても同じことが言えます。
アヤックス

12
Javaラムダは厳密にはクロージャではありません。また、匿名の内部クラスでもありません。それらは、宣言されているスコープ内のすべての変数への参照を持ちません。これらは、実際に参照する変数への参照のみを保持します。これ、参照可能なthisオブジェクトにも適用されます。したがって、ラムダは、単にスコープを持っているだけではリソースをリークしません。必要なオブジェクトのみを保持します。一方、匿名の内部クラスはリソースをリークする可能性がありますが、常にそうではありません。例については、次のコードを参照してください:a.blmq.us/2mmrL6v
squid314

その答えは、単に間違っている
ステファン・ライヒ

@StefanReichに感謝します、それはとても役に立ちました!
ローランドテップ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.