日々常々

ふつうのプログラマがあたりまえにしたいこと。

javapで見るlambda式とメソッド参照のちがい

きっかけ

gakuzzzzさんのBuriKaigiの資料

map / filter などの高階関数よりも古典的な for文の方が読みやすいと感じるあなたへ

BuriKaigiは北陸で行われているITエンジニアカンファレンスで、「勉強した後に美味しいものたべようぜ!」系のイベントです。これ系のイベントはよく行われていたのですが、継続している中で規模の大きい有数のイベントと認識しています。行ってみたいなぁと思いつつ、寒いよなぁ、となっちゃってる私は毎年TLで流れるのを楽しみにしています。

に @earu さんが言及していたのが目に入りまして

資料をみて「あーあるある」と思いながら、幾らかの思考の飛躍を経て

とポストしたら、 @skrb さんが

と来ました。 これに加えて先日全然別件でlambda式とMethodReferenceの違いに対応する実装してたので、書いておこうかなと思ったわけです。

なお多くの業務アプリケーション開発者にはまず不要な知識です。

なにはともあれjavapだ!

さらっとlambda式とメソッド参照を使う同等のコードを書きます。

import java.util.function.*;

record Hoge(){
  void hogeMethod() {}
}

class LambdaExpression {

  void lambda() {
    Consumer<Hoge> lambda = instance -> instance.hogeMethod();
  }
}

class MethodReference {

  void methodReference() {
    Consumer<Hoge> methodReference = Hoge::hogeMethod;
  }
}

javac して javap -v -private 。とりあえずメソッドを見比べます。

// lambda式
  void lambda();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=2, args_size=1
         0: invokedynamic #7,  0              // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
         5: astore_1
         6: return
      LineNumberTable:
        line 10: 0
        line 11: 6
// メソッド参照
  void methodReference();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=2, args_size=1
         0: invokedynamic #7,  0              // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
         5: astore_1
         6: return
      LineNumberTable:
        line 17: 0
        line 18: 6

差はありません。 違いが出てくるのはどちらもしている invokedynamicBootstrapMethods です。

// lambda式
BootstrapMethods:
  0: #32 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #26 (Ljava/lang/Object;)V
      #28 REF_invokeStatic LambdaExpression.lambda$lambda$0:(LHoge;)V
      #31 (LHoge;)V
// メソッド参照
BootstrapMethods:
  0: #29 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #19 (Ljava/lang/Object;)V
      #21 REF_invokeVirtual Hoge.hogeMethod:()V
      #27 (LHoge;)V

LambdaMetafactory とかは読み飛ばしてもらって大丈夫。差分だけ見ましょう。

  • lambda式: REF_invokeStatic LambdaExpression.lambda$lambda$0:(LHoge;)V
  • メソッド参照: REF_invokeVirtual Hoge.hogeMethod:()V

メソッド参照のほうは直接 Hoge#hogeMethod()にいっていますが、lambda式のほうは LambdaExpression.lambda$lambda$0:(LHoge;) にいっています。 こいつの正体はlambda式を書いた時にコンパイラによって生成されるプライベートメソッドです。

// lambda式の方にだけいる
  private static void lambda$lambda$0(Hoge);
    descriptor: (LHoge;)V
    flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #11                 // Method Hoge.hogeMethod:()V
         4: return
      LineNumberTable:
        line 10: 0

javap するときもプライベートメソッドは -private をつけないと出てきません。 javapバイトコードをなんらかの手段で眺めたことはなくても、リフレクションで lambda$method$x というメソッドを 引っかけたことがある人は「あれかー」となるのではないでしょうか。

中は見ての通り、 hogeMethodinvokevirtual してるだけです。 今回はこれで済みますが、lambda式の中が大きくなるとこの lambda$method$x メソッドが大きくなっていきます。要するにlambda式の中身はここに出てきます。

てことで。今回はこうなっています。

  • lambda式
    • 書いたメソッド -> LambdaMetafactory.metafactory -> lambda$method$0 -> 実行したいメソッド
  • メソッド参照
    • 書いたメソッド -> LambdaMetafactory.metafactory -> 実行したいメソッド

注意点ですが、この手のはコードのちょっとした書き方や使うものによって全然違う結果になったりしますし、コンパイラが吐くコードもバージョンその他で変わるものなので、「どんなコードでもこうだし、未来永劫こうである」みたいには捉えないようにしましょう。

あと、java.lang.invoke.LambdaMetafactory なんて多くの業務アプリケーションの開発では目にかかることはないです。Javaじゃない言語を作ってるとか、バイトコードを解析してるとか、なんかよくわからない不具合を踏んだとか、そういうのでもなければ知らないまま過ごしていてなんの問題もありません。 見ての通り javap でメソッドのCodeブロックには出てこず、BootStrapMethodとして一瞬顔を出すだけの子です。

興味が出てきた人向けに、軽い気持ちで見ると「うっ」となると思われるJavadocのリンク貼っておきます。応援してる。

docs.oracle.com

パフォーマンスの話

skrbさんのはネタリプですが、一応真面目な話も添えておきます。

まず差があったとしても誤差です。これを根拠に「パフォーマンスがいいからメソッド参照使いましょう!!」とかいうなら、多分Java使うのやめた方がいい。

きっと他のIOとかで詰まってたり、無駄に1+nでループしたりしてたりする残念な実装の影響が100000000に対して、 lambda$method$x を挟むオーバーヘッドは 1 にもならないんじゃないかなって思います。

パフォーマンスを完全に無視するのは職責に反するので意識するのは重要ですが、パフォーマンスを劣化させるのはごく一部です。こんなとこでは気にしてはいけないとこです。これがボトルネックになっていると分かってから対応しましょう。手遅れにならないうちに計測しましょう。早期に性能測定を行えば無駄なチューニングをせずに済みますし、シフトレフトはパフォーマンスでも有用です。

メソッド参照を推奨したい理由

真面目な話2。

lambda式は {} が不要な1行から3行程度までなら見通しもそう悪くないので使っていいと思います。 また入れ子になっている場合など、lambda式の変数名が必要で仕方なく使用する場面もあります。入れ子にしてる時点で可読性アレですけど。

3行を超えるようであれば、その処理の塊にはきっと名前が付きます。 名前をつけましょう。lambda式にコメントを書くでもいいんですが、メソッド参照にすれば名付けを強制できます。

まず名前をつける。次に「名前をつけるなら」と考える。 ここで冒頭資料のwhatの話が参考になります。

あと「名前は人の頭の中で実行されるコード」の末尾で紹介している「命名のプロセス」も参考になります。

irof.hateblo.jp

私としては「実装パターン」の「対称性」で理解しているもの(lambda式の中身はおそらく外側とコンテキストが違うので対にならない)なのですが、

実装パターン

実装パターン

Amazon

なんか中古12,800円とかなってるし、古典(と呼んで差し支えないだろう)は古典というだけでノイズも多く忌避されるものです。読み方がわかれば全然1980年代の本でも役立つんですけどね……。 なので「Tidy First?」に書いていて欲しいと思うのですが

「シンメトリーを揃える」などの根底になんとなーく流れてそうなのは感じる気がする(気のせいかもしれないけど)くらいで、ダイレクトな記述は見つけられませんでした。残念。

ともかくメソッド参照を使って、名付けをするところから始めましょう。3行を超えるようなlambda式のひとかたまりは名前をつけるに相応しいサイズです。まず塊で識別して、そいつに名前をつけてみる。最初はHowな名前をつけちゃっても構わないと思う。Howがメソッドの内側に隠れたら、使用している方にHowの名前が出ていることに違和感を持てる可能性もある。まずは名前をつけるところから、です。

それはそれとして、試したくなるよね?

Java言語としては lambda$method$0 などは有効なメソッド名であり、自分でも書けます。

動きがわかるように画像で。

まずは lambda$method$1 を定義しました。この時点では問題ありません。 ここでもう一つlamba式を記述すると。

コンパイルエラーになります。 たーのしー。

$ をメソッド名に(というかクラス名とかどこでも)使えることはあまり認識されていないかと思いますが、まぁ使わない方が良いですね。こんな名前は狙わないとつけないでしょうし。

あ、さらにいうと競合するのはシンボルなので、メソッド名や引数が同じだろうと、戻り値型が違えばコンパイルできます。

まったく知らなくていいです。