日々常々

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

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式を記述すると。

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

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

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

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

名前は人の頭の中で実行されるコード

名前を変えても対象そのものは変わらないが、名前を変えると触れる人の振る舞いは変わる。 触れる人の振る舞いが変わった結果、対象がまったく変わらなくても価値は変化するし、対象を変えていくように人が動くようになる。

って話をだらだら書いてみた。

名前重要

xn--97-273ae6a4irb6e2hsoiozc2g4b8082p.com

「名前重要」は「プログラマが知るべき97のこと」のエッセイの一つのタイトルです。2010年の書籍でありますが、経験から得られた知見のエッセンスが多く、中身の具体例はともかく少なくともタイトルは15年経つ今でもほぼほぼ有用なのではないでしょうか。前掲のサイトでも読めますし、紙でも概ね見開きの2ページに収まるものなので、読んだことのない方は読んでみるのもいいんじゃないかなと思います。

横道:名前や名付けに関する話

「うわ、また名前の話か」

となるくらいには、名前や名付けの重要性はよくされている話題かなと思います。 よく話題になるのは「名前を重要だと思っていて、名前の重要性が十分に認識されていないと感じている人」が声を上げることがきっかけなのですが、「名前は重要である」自体は総論賛成各論反対になりがちかつ自分達の身近なものゆえに自転車置き場の議論としても知られるパーキンソンの凡俗法則が発動しやすいものなことと、「十分に認識されていない」と感じるのは残念な名前によって苦しい思いをした結果の憤りの噴出などもあり、主張する人は頑なになるのも当然な構図があるかなと思います。一方、「名前にこだわって全く物事が進まない」のような状況も散見されるわけで、その煽りを受ける人は「名前なんてどうでもいいから進めようよ」と思うのも当然でしょう。特にその議論(議論になっていればまだいいんですが)によって費やされる時間の不利益を被る人は「名前なんてどうでもいい」という思いを強くします。噛み合わないのは自然な構図。この対立の構図として見ると、まれに正面衝突はしますが、広いコンテキストでは「名前は重要である派」が強い主張をし、「名前なんてどうでもいい派」は「そうは言ってもなー」と思いながら聞き流すという形ではないでしょうか。そして聞き流されていると感じると、主張を続けることに……燃料切れまで走り続ける非生産的な何かになってしまい以下略。(このブログもその一種なわけだ)

この辺りで横道から帰ってこよう。

「名前は人の頭の中で実行されるコード」と考えて取り組む

他の人に強要するものではなく、私が自分に適用する考え方です。 やれとは言わない。やるなとも言わないけど。 あ。名前を処理するソフトウェアはありますが、ここではそれの話は除外します。

名前は人の頭の中で実行されるコードだと思いました。 変数名、メソッド名、クラス名、パッケージ名、モジュール名、アプリケーション名、サーバー名、システム名、チーム名、プロジェクト名、プロダクト名、ロール名、、、開発していると名前をつける機会がたくさんあります。名前は人間同士のコミュニケーションで対象を識別するのが第一の目的になりますが、識別だけでなく使用した人の頭の中で処理され、人の動きに影響を与えます。

雑な名前がつけられたものは雑に扱われますし、重要そうな名前が付けられたものは重要に扱われるものです。 機械的な名前であれば「コンピュータが扱うものだな」と認識され、コンピュータの内部実装に関わらない人(たとえばシステムのユーザー)にはその存在を伝えないようになります。 記号+連番(たとえばA001,A002,,,B001といったもの)であれば、「A001とA002は同時に見てB001は見ない」という動きを促進しますし、「A002とB001を同時に見るがA001は見ない」のような行動を抑止します。(これはもともと関連の強いもののグループにAとかBで分けているという側面もありますが、そのことを知らなくてもそういうふうに動くようになるという話です。) ドメイン駆動設計におけるユビキタス言語(同じ言葉)が使われているものであれば、変更する際に変更の共有が促進されます。

もちろん自然言語で処理されるため、名前で万人の行動を制御できるわけではありません。 異なる処理の仕方をする人もいるでしょうし、全く気にしない人もいます。実に柔軟で再現性や確実性の低い処理系ではありますが、少なくとも名前によって前述の例のような力は働きます。

名前によって動作を制御する。 この構図はプログラマが普段書いてるコードと同じです。 「こう処理して」と書いてコンピュータに伝えるのがコード。「こう扱って」と人に伝えるのが名前。 書いたコードは後で処理され、影響を与え続けます。名前もそれに触れる人に影響を与え続けます。同じ同じ。 (「文章は全部そうだ」「そもそも文字が」って話にすっ飛んでいきかけたけど、帰って来れなくなる確信があるので思考を止めることにした。)

であれば、影響を与え続けるものを作ることに慣れているプログラマは、同じ考え方で名付けをできるのではないか?と思うわけです。 また、プログラマは名前をつける機会が非常に多いです。名前を考える職業は他にもありますが、毎日何十もの名前をつける必要がある(機械的につけられちゃいますが)なら、うまくなる機会も多いわけで。

「やらないことはうまくならない」 は最近好んで使用するフレーズですが、名付けに関してはプログラマは「やらない」が無いんですよね。とはいえこの言葉は「やればうまくなる」ではなかったりするんですが。 毎日何十もの名付けをしているにも関わらず、名付けが苦手な人は多いです。私も得意とは正直いえません。「やらないことはうまくならない」に近い言葉として「量が質に転化する」なども挙げられるのですが、質に転化するためにも条件があるんですよね。経験が熟達につながるのは、経験にバリエーションがあるときです。全く同じ経験を繰り返していても、上手くなるのはその1つだけです。「名付け」という広い範囲では「日々膨大に行っている一つ一つの名付けから異なる経験値を得る」というアプローチが取れます。そもそも名付けを行う機会が少ない職業と比べると「機会が膨大にある」というチートを得ているんだなぁ、と。他にも熟達の重要な要素として「失敗からの学び」があります。他の名付けは失敗した時のダメージが大きく、失敗しないように慎重につける必要はありますが、プログラマにはリファクタリングの名前変更に代表されるように、失敗しても再チャレンジがしやすいです。成功だけでなく失敗のフィードバックループもぶんまわせるのは条件が整いすぎに思えます。

また、「人の動きを制御するための命名」と考えると、名付けにおける空中戦やプラクティスの競合にも対処できる気がしています。 「コードを読む時間を減らして他の重要なことにより多くの時間を割けるようにする」のであれば可読性の高い名前を設計することになります。 「すべての名前にこだわっていたら時間が全然足りない」とかも「その名前を見る人はどういう人で、その人の動きをどう制御したいか」を考えれば「こだわる(悪い意味で使われることが多いなぁ)」対象を選べるようになる気がします。「ローカル変数の名前はどうでもいいんだ!」のようなものを盲目的に採用するのではなく、プラクティスの採用の説明をできるようになるかなと。

名前は願いであり、呪いでもあると言われます。こどもの名付けでも「こういう人になってほしい」というふうに付けるという話も聞きます。名前によって変わるのは当人ではなく周囲の人たちの行動で、その行動が当人に影響を与える構図です。 システム開発における名前も同様です。クラス名をつけたときは何にしようが全く関係ありませんが、その名前を見た人が、名前をつけた方向に成長させていくようになります。名前をつけられたものが勝手に成長するわけではなく、見た人の行動を制御しているのがポイントです。これはクラス名に限らず、アプリケーション名、チーム名、定例会議名、いずれも同じです。

名前が人の頭の中で実行された結果起こることについてはこれくらいにして、「名前で万人の行動を制御できるわけではない」をみてみます。

重要な点として自然言語であり、ウェットウェア(人の脳のことをこう表現することがある)で処理されることが挙げられます。 名前そのものにどれだけ注力しようとも、名前はつけた時に実行されるわけではないですし、コンパイルされるわけでもありません。 名付けだけでは影響力は決まりません。名前というコードは、つけられてから時間をおいて、人の頭の中に入って初めて動作します。

ウェットウェアは千差万別で制御不能、として諦めるのも一つの考え方ですが、少なくとも仕事としては名前に触れる範囲が制御されている(コンテキストや区切られた文脈に近しい。私は「名前には射程がある」と表現しているのですが、あまり伝わってる感触はないです。なんとなくコンテキストとかだとコンテキスト内は同じ濃度な感じがするんだけど、減衰すると思ってるので射程って言ってる。)わけですから、その範囲に限定しての影響力は観測できます。観測できれば影響力に働きかけることはできます。影響力を上げる方法の一つが「名前の重要性」を説いたいくらかの書籍や言説を共有することでしょう。まず「名前に良し悪しがある」のようなことを共通認識とするところからかもしれません。

名前の影響力への働きかけ方は色々あります。 レビューなどで名前の話を出さないようにするのは、名前の影響力を下げる有効な方法です。 ネーミング規則を整備して強制力を強めると、規則に従うのが前提であれば規則通りの名前であることへの影響力は高まり、規則内の予測可能性は向上する一方、規則外のものは存在そのものが否定されると言った影響力が強まります。 IDEなどが自動生成する名前を盲目的に選択していると、「IDEがつけた名前」と認識されるようになり、意味のない名前として扱われ、その部分に関しては名前の影響力は低下します。 いくつか書いてみましたが、上げる方法が「名前について口酸っぱくいう」くらいしか思いつきません。他もあるという確信はあるので、ループ図でも書いたら出てくるんだろうけど。今はいいや。 名前の影響力を上げるも下げるも自分達の振る舞い次第です。影響力をあげれば名前はすごく強力な道具になります。影響力が上がれば効果を発揮してさらに影響力が上がるというポジティブループはあるはずです。

とりあえず「名前もコードなんだからプログラマは取り組みやすいしうまくなりやすいんじゃないかなぁ」と思ったのを書き殴ってみました。 とっ散らかってるし、色々破綻してると思うけど、こんな感じです。AIに書き直してもらおうかと思ったけど、AIを通して要約するとかはいつでもできるし、原液のままで晒しておきます。選択肢は広くとって、情報の欠落はさせず、最終責任地点は後ろに持っていくものです。とか言っておくとそれっぽい。

紹介

良いコード/悪いコードで学ぶ設計入門

「名前設計」として1章まるまる割かれています。「名前設計」は好きな言葉です。

初版では10章、改訂新版では11章ですね。 たとえば本稿の解釈では「悪魔を呼び寄せる名前」は見た人に対して「悪魔を召喚せよ」と指示する名前になります。

命名のプロセス

scrapbox.io

最新版?: https://www.digdeeproots.com/articles/naming-process/

かわしまさんが3年前?に翻訳してくれたもので、よく参照します。 「一発でいい名前になんてならないので、こんなプロセスでやってみようぜ」というもの。段階にわけ、段の進み方を定義する。好きなやり方です。

irof.hateblo.jp

これはMissingをNonsenseに進めるアプローチですね。書いている内容の通りで、ミスリードする名前や名前がなくて識別すらされていないMissing状態よりは前に進められています。 一方で安易にHonestにするのを避けています。進化的ネーミングのプロセスを取れていて低コストにHonestにできるならやればいいのかもしれませんが、そこで立ち止まることも多く、半端な「誤魔化した名前」になりがちなのでブレーキを踏んでいるもの。

SpringBoot3.4の構造化ログ #kanjava

関ジャバ'25 1月度で話したスライドと軽いコメント、イベントの話です。

speakerdeck.com

  • 構造化ログが標準機能に入った
  • 構造化ログを使ったことがない人は始めるいい機会
  • 久々のテクいセッション(ただし需要)

概ね書いてるので補足はいらないかなと。

とりあえずやってみる程度であればスライドに書いているのでいけると思いますが、プロダクトで対応する際はSpringBootのリファレンスを読んでください。場所は Spring Boot / Reference /Core Features / Logging / Structured Logging です。

SpringBootのコードを読みたい方は、主に以下の org.springframework.boot.logging.structuredorg.springframework.boot.json です。「コードはIDEで読むでしょ」って思っていましたが、今回くらいのであればブラウザでも読める程度かなと思います。

紹介しているサービスデザインパターンはこれ。2012年の本で、新品はなさげ。そんな厚くない&読みやすかったので好きなんですけどねぇ。

サンプルコードは……実用的なのないし下手に真似られてもーなんで、スライドだけに留めます。

スライドに乗せてるMDCの解放について。 irof.hateblo.jp

あとMDC使う時のスレッド間引き継ぎ云々の話は「そもそもやりたいこと」あたりで書いてます。 irof.hateblo.jp

ハイライト

主催側が一部にだけ言及するのってどうなんだろうとか思ったりしなくはないんだけど、全部書いてるとあれなので許してください。どれも面白かったです。やっぱご時世柄か生成AIの話が多かったんです。

  • 【朗報】Spring AIを使えばJavaエンジニアでも生成AIと遊べるぞ
    • 一番最初のバージョンが出た時にチラッとみて「うーん?」って思ってたんですが、使いたくなるくらいまで来ていました。これは救世主(メシア)。
    • 「※MCPまわりは絶賛コードリーディング中なので説明が雑」とサラッと読むのが前提になっているあたり、久々だったけど「うらがみさんはうらがみさんだなぁ」と安心しました。
  • AIエージェントとBowlingGameKataやってみた
    • 一発で答えに行くの生成AIあるあるで、TDDのベイビーステップと歩幅合わないのなるほど感。まぁ人間もそういうのしがちだし、ステップひとつひとつの学習とかもそういう観点じゃしなさそうだし、そういうもんかなぁと。
    • 動いてるのをしっかり時間とって見たいやつでした。

イベントについて

「スピーカーはベテランばっかだし」とゆるふわでやってたら、全員30分じゃ終わらない(どうみても各々1時間コースでしょこれ)ものを携えてきていました。 中身スカスカでショートするよりはいいんですが、全員一部スキップみたいな感じになり、2セッションにしとけばよかったかなぁとか思いました。 セッション募集枠もうひとつとってたけど、入らなくてよかった(入ってたら事前調整もうちょっとちゃんとしたと思うけど)

LTを試験的に入れてみました。 進行が雑で時間すぎちゃったとか、カウントダウンの仕方がスコーンと抜けてしまってて、ぐだぐだでした。ほんと申し訳ない。

MOTEXさんに200人の会場をお貸しいただきました。ありがとうございます。 なんと座席の1割も埋まりました。前回お借りした時から倍増です。やったね(目を逸らしながら