日々常々

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

getとfindの使い分け

メソッド名の getXXXfindXXX どっちがいいの?みたいな話になることがある。

この手の話ができるだけでもいい感じだと思います。名前が記号化していないってことなので。 世の中には名前に力を割くのが無駄な文脈もあって、そう言うのに晒されて続けると当然そこに力をかけなくなります。 その文脈では最適解だけど、私は名前が重要だと思っているし、その価値観を土台に他のものを積み上げていきたい。 ということで話を戻す。

「得る」と「探す」のようなものを意図して使い分けるとコードが読みやすくなります。 使い分け方によって読みやすさは変わりはするのですが、「意図して使い分けている」だけでも十分変わります。 その意図に共感できたり汲み取れたりすればさらに読みやすくなりますが、その前段階として意図の有無が重要だと思ってます。

私の基本的な使い分け

  • getHoge(条件): Hoge
  • findHoge(条件): Optional<Hoge>

戻り値型に現れます。getOptionalはない感じ。

getXXX は有ることが前提。無い場合は例外的事象として例外で扱います。 このメソッドの呼び出し側に見つからない場合を考慮させるべきではないので、非検査例外とします。

findXXX は無い場合が例外的事象ではないので、戻り値で扱います。 戻り値 Optional として、呼び出し側にハンドリングを要求します。

いま null にするのは?

私が null 根絶穏健派(過激派じゃないよ)なので「使うな」にはなるんですが。

上記のような文脈の使い分けをするとして、見つからない場合に null を返すのは避けることを推奨します。 getHoge(): HogefindHoge(): Hoge が並ぶと、使い分けが分かりません。 この場合はドキュメントでのフォローになりますが、コンパイル時の検査が効かなくなり、Javaを使っている利点が減衰してしまいます。

事実 null を返すAPIはドキュメントでフォローされています。 たとえば java.util.Map#get(Object) が挙げられ、このメソッドを使用する側は null を考慮した実装をコンパイラではなく人間の注意深さでフォローする必要があります。 NonNull のようなアノテーションとか静的解析、IDEのチェックなどで機械的なチェックがフォローされる範囲は広がってはいますが、まだまだ。

Optional がない時代、 getXXX は同じで findXXXnull を返すか検査例外のいずれかで、慣習的には null が返されていたものが多かったように思います。 いまは Optional があるのでこちらで扱いましょう。完璧な解ではないですが、 null よりは良いです。

よそ見: Optional自身のgetの使い方

Optional には find とかないですが、 getempty の場合に非検査例外を投げます。

ただ get だけの名前だと危険が伝わりづらいため orElseThrow(Supplier)を使いましょう と書きました。こちらはJava8の頃(2015年5月)でしたが、 Supplierの記述がだるいのは理解できます。それもあってか、引数なしの orElseThrow() メソッドがJava10(2018年3月)で追加され、 Optional のJavadocでもこちらを使うように案内 されています。

なんとなく get メソッドがあるクラスで Optional を連想したのであげてみたけど、 Optionalfind は合わなさそう。もし使われてたとしたら、 find はメソッド呼び出し時に解決しにいくようなイメージを持ちそう。

よそ見: Jacksonのgetとfindの使い分け

Jacksonの JsonNode を見てみましょう。Javadocはこちら。以下のようなメソッドがあります。

  • get(String fieldName): JsonNode
  • findValue(String fieldName): JsonNode

いずれも JsonNode を返しますし、いずれも null を返します。違いは get が直下のみを探すのに対し、 findValue は子要素も探すこと。スキーマ不定でないなら基本的に get のみを使用するで良いと思います。

Jacksonのこの使い分けには「探す場所として指定されたパスに子要素も含むでしょ」のようなJSONがパスで表現できることを前提にしたAPIって感じですね。 そのライブラリがどういう世界観で言葉選びをしているかの過程を持っていると予測可能性が上がります。

よそ見: XXX

「よそがこうしているから自分たちもこうする」と言うのは「よそ」がいくら権威的なものであろうと微妙です。 よそはよそ。

でも「よそは(こう言う文脈だから)こう使ってるんだなぁ」と想像したり説明を試みてみるのは役立つと思います。 開発で使う語彙は中学英単語よりも少ないと思うし、目に入ったら眺めてみるといいと思う。

他の名前の候補

get でも find でもない何かいい感じの名前が見つかるならそれを試してみるのもいいです。 search はたまにみる。

読みやすいコードのガイドライン 第2章 命名 では get find search pop calculate fetch query load などが挙げられています。これらの単語で置き換えできるかを試してみると名前が引き締まったりする。

書籍では「辞書や類語辞典」が挙げられていて、私はこういうのは英英辞典をよく使います。

search を使うのは find より探し回る感とか検索条件が広そうなイメージある。とは言えネイティブでない我々がニュアンスで使い分けるのは難しいので、チームやそのコードに関わるスコープで会話しながら調整したいところ。

参考

先に挙げた「読みやすいコードのガイドライン」や「良いコード/悪いコードで学ぶ設計入門」など、最近(といってもともに2022年ですが)の書籍でも命名はそれなりに割かれています。

同じようなことや逆のことを書いているように見えたりして面白くあります。私も共感するものもあれば、趣味に合わないなぁと思うところもしばしば。この2冊はGod/Badのような書き口。

名前の話は長年ずっと語られることで、10年遡ってみても「CleanCode」(邦訳2009年)では第1章「クリーンコード」の次にくる第2章が「意味のある名前」です。

あと「きのこ本」として知られていた「プログラマがしるべき97のこと」(2010年)の「名前重要」とか。

20年、30年遡ってもいくらでもあると思うんで、興味ある人は探してみよう。

前述のように英単語の選択に迷う場合は英英辞典ですが、用途的にポケットサイズのを使っています。Webでもいいのですが、周りの単語がヒントになることも多いので紙が好き。

あとニュアンスはこちらのニュアンス図鑑もいい感じです。 make build create produce とか。

ニュアンスだとコアイメージもたまにみてる。

別に英語的な正しさとかは重視しているわけではなくて、意図して使い分けるための根拠を私はこの辺に求めている感じです。 日本語名称の時は国語辞典のほか、語源とか漢字の由来とかを参照したりする。そいやこっちは書籍とか持ってないな……。

私はいい名前が思いつかないときは変な名前をつける - 日々常々 とブログで書いているように、名前はいきなりは思いつかないしどんどん変えて育てていくものだと思うのでプロセスも紹介しておく。

scrapbox.io

このプロセスの過程で一般的には「避けるべき」とされる名前が出ることもあるだろうし、すべての名前をこのプロセスでやるものでもないと思っている。

と言うことで、名前の使い分け

意思に基づいて名前が使われていることを望みます。

それは「同じものは同じ単語を使う」と言う形で現れるかもしれないし、他の形かもしれない。 ともかく意思に基づいていることが重要で、「可読性」がもたらす「予測可能性」を重視したいと思っています。 読みやすいコードとは驚きのないコード。驚きのないコードとは予測が当たるもの。そんな感じ。

get/find の本稿の使い分けはそれなりに受け入れられていると思うので扱いました。 こうしておくとコードを読むときに挙動の予測ができて紛れが減ります。 可読性に寄与すると言われる一般的なルールに反していても、予測可能性があれば読みやすいコードと言えると思う。

使い分けに迷ったら自身の経験を言語化してみたり、他のライブラリの使い方を参考にしてみるのもいいと思う。 でもその使い分け方が貴方のコンテキストに合うかは別の話。あくまで参考に。 Jacksonの例であげたように文脈が違えば使い分けで示したいものも変わってくるので、ライブラリやフレームワークを使う時にはその文脈の言葉をどこまで持ち込むかを設計する必要がでてきます。ものによってはコンテキストマッピングが必要になるかもしれない。

そんな感じ!(まとまらない)

追記

「基本的な」使い分けと書いている通り、他の使い分けをすることもあります。 重要なのは使い分け方が同じ文脈で混ざらないことかなーって。 多分言葉を尽くせば根源にある共通的な概念を言語化できるんだけど、しなくても伝わるでしょ、うん。

計算量

私も同じ感覚ある。

件数

私も同じ感覚ある。 get1 に対して findList0..n と言う感覚。 件数での使い分けを考えると 0..1, 1 0..n 1..n を分けたくなって、 get / find だけじゃ足りねぇとなり、戻り値型とか findOne とか他の表現方法組み合わせてなんとかしようってやり始める。毎回何だかんだそれなりに落ち着くんだけど、決まった答えは持ってないかも。

コードを人に寄せたい(という題の書きかけ)

ブログの下書きを眺めてたら書きかけで飽きたっぽいのがみつかったので供養しておきます。


もう二年前になりますが、「コードをどまんなかに(該当スライド)」でこんな風に人とコンピュータとコードの関係を話しました。(なんかこれのブログ書いてないな……)

ここから少し掘り下げて、コードをソースコードバイトコードに、コンピュータをコンパイラとランタイムに分わけてみます。 「わけるとわかる」とか言いますが、わけるとこんな図が描けます。

ソースコードは人とコンパイラに関係します。 バイトコードコンパイラとランタイムに関係します。 関係に依存方向を入れてみたのがこの図です。

ちなみに言語仕様とかは意図的に省いています。主眼じゃないものを取り除いて強調したいものを強調するのがモデリングだと思ってる。

irof.hateblo.jp

言語仕様入れるとこんな感じ?言語仕様とコンパイラは相互依存になるかもだけど。どちらにしても今回の話には関係ないです。

元の図に戻って、ソースコードから人に依存させているのは、人の読み書きのしやすさに依存して欲しいから。(読み書きのしやすさと文脈だとIDEとかが間に入ったりするんだけど、それも別の話。) ソースコードコンパイラには依存せず、コンパイラソースコードをなんとかしてバイトコードにする感じです。

せっかくこんな構造になってるんだから、ソースコードは全力で人に依存していいはずなんです。 にもかかわらず、ソースコードにはコンパイラ都合が入り過ぎていたり、なんならランタイムの都合も入っていたりします。 それらが入ってしまうと可読性や記述性が損なわれるのは当然。人の方向を向いていないわけですから。

で、一つの解にDSLがあったりするわけで、外部DSLだと言語仕様への依存を外せる。


2024-05-13T19:17追記

ここから話広がりそうなんだけど、なんか終わってんだよねぇ。ブログのタイムスタンプによると最終更新が 2020-06-26 23:25:56 らしい。

なるほど。捨てたつもりが捨てきれてなかっただけっぽい。下書きに残してるなんて未練がましいので晒してやる。

いつか気が向いたら書き直すよ。いつか。

2021年のJavaとnullの話

ブログの下書きを眺めてたら長文がみつかったので供養しておきます。 TODOがDOされるかはわかりません。心の目で見てください。


Javaには null があって、この子がいない世界がいいんですけど、いなくなったりはしてくれないので、仕方がありません。いい感じに付き合っていきましょうねーって話を書きます。関連エントリとかスライドはこの辺。

以降は長文ですが、この手のは実務でいくつかの条件を満たした上での話になります。 なるべく書いたつもりですが、無意識に前提としているものはスコーンと抜けているでしょうし、まるっきり同じ条件でも「こちらの方がいい」という選択肢もあっていいと思います。その辺の話は実務でやりたいなーって思ってます。

本稿は「Javaの話」として書いてます。「他の言語にすればいい」とかは異なる話です。

私の基本的な考え

NullPointerException(以降NPE)は起こるべきところで起こって良い。ダサかろうと予期しない null が混入した際に発生する適切な例外はNPEであり、他の何かで取り繕う必要はない。

予期しない nullnull チェックで誤魔化さない。null チェックして null を返すようなものは延命措置にしかならないし、そんな延命してる間にどんどん傷は深くなる。フェイルファスト。

そのような null は混入しうる境界で検疫し、丁寧に取り除く。実装は冗長で良い。下手に格好良く null を回避できると、一律それを行うような思考停止を招きかねない。 null は必要なところでだけで、泥臭くダサく目立つ形で取り扱う。

もう少し詳しい話

(なんかどっかで書いたと思うんだけどなぁ、と思いながら)

null が混入するタイミングはそれなりに限定的であり、予測可能です。

  • 自身で null を明示的に使用した場合。変数へ代入したり、メソッドの引数や戻り値にすることで伝染する。比較での使用は問題ない。
  • 参照型フィールドのデフォルト値。
  • フレームワークに設定される引数。
  • ライブラリのメソッドの戻り値。

このうち前者2つは自身の実装の話です。フィールドのデフォルト値は完全コンストラクタに拘れば、自身が混入させるのは null を明示的に記述した場合に限定されます。コンストラクタ引数に null が勝手に突っ込まれるのは、後のフレームワークやライブラリの話。

脱線: ちなみに JIGバイトコードから null を見つけて警告しています。自身のコードによる混入チェッカー。作者の null 嫌いがわかりますね(ただの自己紹介)。

自身で制御しづらいのは後者の2つですが、フレームワークやライブラリの実装都合との接点で null は片付けてしまうように設計します。 最近のフレームワークやライブラリは null の代わりに Optional を選べるものも増えてきましたが、これは別に解ではありません。てか Optional であっても考慮しなきゃいけないことには代わりないので、可能であれば null もemptyな Optional も取り除き、予期しない値であればここで素直に例外を発生させます。予期できる null であれば、適切な取り扱いをこの境界で決定し、コアである処理には持ち込まないようにします。

<<<TODO 例示>>>

null は言語都合や実装都合で混入するものなので、コアなところに null は入ってきて欲しくないわけです。Javaという言語上どこにでも入りうる null をそのままにしておくと、本当にやりたいことが null の考慮に圧迫されます。考えなければならないことを減らすが設計の基本だと思っているので、安全な領域を定めて境界で弾く。弾き損ねたものは素直にNPEのフィードバックを受け取る。そんな感じです。

nullセーフな演算子

たとえばGroovyには ?. があります。 null だった場合も気にせずにメソッドが呼び出すかのように記述でき、 nullの場合は呼び出されずにnullが返る便利さんですね。他の言語でも似たような機能は結構見ると思います。局所的にはシンプルなコードが記述できます。

これは少なくとも私がJavaで業務処理を書く時には存在していて欲しくないものです。 道具が悪いのではなく使い方の問題ではあるんですが、「hoge.method() と書けるものであっても hoge?.method() と書いておけば安全」のような思考停止を招くからです。一律やるのは避けたとしても、「NPEが発生したら .?. に書き換える」ようなことが起こります。私は意志が弱いので、その引力に負けない自信はありません。そしてきっと後悔する。

これは「強力な機能が設計の歪みを覆い隠す」パターンだと思っています。 null が混入しなくなった場合に除去するフォースも働きません。そしてそのコードがある限り、 null は混入しうるのか?と疑心暗鬼になります。1箇所あるんだから、他でも入ってきたりするんじゃ……と。そんなノイズを入れながらコードを読み書きしたくないんだ。

でももし言語機能として持ってたら、特定の文脈(ライブラリとかフレームワーク作ってる時とか)では喜んで使うと思う。便利は便利だからねぇ。

JEP 358: Helpful NullPointerExceptions

2021年風味を混ぜておきます。 Java14で入ったJEP 358は、今年9月リリース予定であり、しばらくのスタンダードになると思われるJava17で使用できます。 これは何かというと、メソッドをチェーンしている場合などの途中でNPEが発生した場合、どれが null かを教えてくれる機能です。 以下は 16.0.0-librca での実行例です。

jshell> class A { Object obj;}
|  次を作成しました: クラス A

jshell> var a = new A()
a ==> A@5f184fc6

jshell> a.obj.toString()
|  例外java.lang.NullPointerException: Cannot invoke "Object.toString()" because "REPL.$JShell$12.a.obj" is null
|        at (#3:1)

jshell>

Java13以前で a.obj.toString() のようなコードでNPEが発生した場合、スタックトレースには行番号しか表示されず、anull なのか objnull なのかわかりませんでした。そのためNPEの解析では一時変数で受けてみたり、aa.obj それぞれに対して null チェックを行ってみるなどの足掻きが必要でした。デバッガなどを使用しない場合、コードを変えないと解析が困難なこともあり(少なくともスタックトレースから瞬時に判断はつかない)、NPEが嫌煙される一因だったかと思います。

Java14以降ではObjects#requireNonNull(Object) メソッドや自身で null チェックしての例外送出より、そのままNPEを起こした方が解析しやすいまであります。予期しないNPEには備えるんじゃなく、予期しないnullを教えてくれるNPEを素直に受け止めるのがいいと思います。

nullを許容する場合

ライブラリが要求するAPIになっている場合と、閉じたスコープで使用する特殊値です。

クラスに閉じたフィールドで、最も軽量に扱える特殊値はおそらく null であり、それ以外の値を使用するのはコストが上回ります。 ただその null が外部に流出しないように設計/制御する必要はありますが、制御可能な範囲において null 以外を頑張って使用するよりかは扱いやすいです。これは単に null が言語に組み込まれた特殊値であるが故で、使えるから使うと言うスタンスです。決して「 null が最適解」ではないので、誤解されぬよう。。。

まとめ

NPEがダサいって気持ちはわからなくはないですが、NPE以外の予期しない実行時例外もどうせダサいです。NPEじゃなくす努力なんて要らないんじゃないかなって。

あー null 無くならないかなー。

おまけ(2024-05-12T0:03)

投稿したらサムネに出た画像をおまけでつけておきます。

たぶん <<<TODO 例示>>> でこうやって棲み分けるんだーみたいなことを書こうとしてたんだと思います。 ファイル名が 20210421013620.png とかから察するに、あれがアレで。もう覚えてない。 Miroで書いてるっぽいからどこかのボードに残骸はあるんだろけど……