getとfindの使い分け
メソッド名の getXXX
と findXXX
どっちがいいの?みたいな話になることがある。
この手の話ができるだけでもいい感じだと思います。名前が記号化していないってことなので。 世の中には名前に力を割くのが無駄な文脈もあって、そう言うのに晒されて続けると当然そこに力をかけなくなります。 その文脈では最適解だけど、私は名前が重要だと思っているし、その価値観を土台に他のものを積み上げていきたい。 ということで話を戻す。
「得る」と「探す」のようなものを意図して使い分けるとコードが読みやすくなります。 使い分け方によって読みやすさは変わりはするのですが、「意図して使い分けている」だけでも十分変わります。 その意図に共感できたり汲み取れたりすればさらに読みやすくなりますが、その前段階として意図の有無が重要だと思ってます。
私の基本的な使い分け
getHoge(条件): Hoge
findHoge(条件): Optional<Hoge>
戻り値型に現れます。get
でOptional
はない感じ。
getXXX
は有ることが前提。無い場合は例外的事象として例外で扱います。
このメソッドの呼び出し側に見つからない場合を考慮させるべきではないので、非検査例外とします。
findXXX
は無い場合が例外的事象ではないので、戻り値で扱います。
戻り値 Optional
として、呼び出し側にハンドリングを要求します。
いま null
にするのは?
私が null
根絶穏健派(過激派じゃないよ)なので「使うな」にはなるんですが。
上記のような文脈の使い分けをするとして、見つからない場合に null
を返すのは避けることを推奨します。
getHoge(): Hoge
と findHoge(): Hoge
が並ぶと、使い分けが分かりません。
この場合はドキュメントでのフォローになりますが、コンパイル時の検査が効かなくなり、Javaを使っている利点が減衰してしまいます。
事実 null
を返すAPIはドキュメントでフォローされています。
たとえば java.util.Map#get(Object)
が挙げられ、このメソッドを使用する側は null
を考慮した実装をコンパイラではなく人間の注意深さでフォローする必要があります。 NonNull
のようなアノテーションとか静的解析、IDEのチェックなどで機械的なチェックがフォローされる範囲は広がってはいますが、まだまだ。
Optional
がない時代、 getXXX
は同じで findXXX
は null
を返すか検査例外のいずれかで、慣習的には null
が返されていたものが多かったように思います。
いまは Optional
があるのでこちらで扱いましょう。完璧な解ではないですが、 null
よりは良いです。
よそ見: Optional自身のgetの使い方
Optional
には find
とかないですが、 get
は empty
の場合に非検査例外を投げます。
ただ get
だけの名前だと危険が伝わりづらいため orElseThrow(SupplierSupplier
の記述がだるいのは理解できます。それもあってか、引数なしの orElseThrow()
メソッドがJava10(2018年3月)で追加され、 Optional
のJavadocでもこちらを使うように案内 されています。
なんとなく get
メソッドがあるクラスで Optional
を連想したのであげてみたけど、 Optional
に find
は合わなさそう。もし使われてたとしたら、 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
とか。
ニュアンスだとコアイメージもたまにみてる。
別に英語的な正しさとかは重視しているわけではなくて、意図して使い分けるための根拠を私はこの辺に求めている感じです。 日本語名称の時は国語辞典のほか、語源とか漢字の由来とかを参照したりする。そいやこっちは書籍とか持ってないな……。
私はいい名前が思いつかないときは変な名前をつける - 日々常々 とブログで書いているように、名前はいきなりは思いつかないしどんどん変えて育てていくものだと思うのでプロセスも紹介しておく。
このプロセスの過程で一般的には「避けるべき」とされる名前が出ることもあるだろうし、すべての名前をこのプロセスでやるものでもないと思っている。
と言うことで、名前の使い分け
意思に基づいて名前が使われていることを望みます。
それは「同じものは同じ単語を使う」と言う形で現れるかもしれないし、他の形かもしれない。 ともかく意思に基づいていることが重要で、「可読性」がもたらす「予測可能性」を重視したいと思っています。 読みやすいコードとは驚きのないコード。驚きのないコードとは予測が当たるもの。そんな感じ。
get
/find
の本稿の使い分けはそれなりに受け入れられていると思うので扱いました。
こうしておくとコードを読むときに挙動の予測ができて紛れが減ります。
可読性に寄与すると言われる一般的なルールに反していても、予測可能性があれば読みやすいコードと言えると思う。
使い分けに迷ったら自身の経験を言語化してみたり、他のライブラリの使い方を参考にしてみるのもいいと思う。 でもその使い分け方が貴方のコンテキストに合うかは別の話。あくまで参考に。 Jacksonの例であげたように文脈が違えば使い分けで示したいものも変わってくるので、ライブラリやフレームワークを使う時にはその文脈の言葉をどこまで持ち込むかを設計する必要がでてきます。ものによってはコンテキストマッピングが必要になるかもしれない。
そんな感じ!(まとまらない)
追記
「基本的な」使い分けと書いている通り、他の使い分けをすることもあります。 重要なのは使い分け方が同じ文脈で混ざらないことかなーって。 多分言葉を尽くせば根源にある共通的な概念を言語化できるんだけど、しなくても伝わるでしょ、うん。
計算量
改めて考えると、自分は以下みたいなイメージで使い分けている気がする。
— 大雪 命 (@mikoto2000) 2024年5月30日
get ... O(1)
find ... O(x) : x > 1
が、ちょっと考えるだけで例外ある気がするなぁ... https://t.co/eh7AKyhH34
私も同じ感覚ある。
件数
良かった。
— shiryu (@shiryu_go) 2024年5月30日
自分の場合は今まで、
findだとList
getだと単一
が帰ってきそうなイメージだった https://t.co/HvuMV23w0O
私も同じ感覚ある。 get
が 1
に対して find
は List
で 0..n
と言う感覚。
件数での使い分けを考えると 0..1
, 1
0..n
1..n
を分けたくなって、 get
/ find
だけじゃ足りねぇとなり、戻り値型とか findOne
とか他の表現方法組み合わせてなんとかしようってやり始める。毎回何だかんだそれなりに落ち着くんだけど、決まった答えは持ってないかも。
コードを人に寄せたい(という題の書きかけ)
ブログの下書きを眺めてたら書きかけで飽きたっぽいのがみつかったので供養しておきます。
もう二年前になりますが、「コードをどまんなかに(該当スライド)」でこんな風に人とコンピュータとコードの関係を話しました。(なんかこれのブログ書いてないな……)
ここから少し掘り下げて、コードをソースコードとバイトコードに、コンピュータをコンパイラとランタイムに分わけてみます。 「わけるとわかる」とか言いますが、わけるとこんな図が描けます。
ソースコードは人とコンパイラに関係します。 バイトコードはコンパイラとランタイムに関係します。 関係に依存方向を入れてみたのがこの図です。
ちなみに言語仕様とかは意図的に省いています。主眼じゃないものを取り除いて強調したいものを強調するのがモデリングだと思ってる。
言語仕様入れるとこんな感じ?言語仕様とコンパイラは相互依存になるかもだけど。どちらにしても今回の話には関係ないです。
元の図に戻って、ソースコードから人に依存させているのは、人の読み書きのしやすさに依存して欲しいから。(読み書きのしやすさと文脈だとIDEとかが間に入ったりするんだけど、それも別の話。) ソースコードはコンパイラには依存せず、コンパイラがソースコードをなんとかしてバイトコードにする感じです。
せっかくこんな構造になってるんだから、ソースコードは全力で人に依存していいはずなんです。 にもかかわらず、ソースコードにはコンパイラ都合が入り過ぎていたり、なんならランタイムの都合も入っていたりします。 それらが入ってしまうと可読性や記述性が損なわれるのは当然。人の方向を向いていないわけですから。
で、一つの解にDSLがあったりするわけで、外部DSLだと言語仕様への依存を外せる。
2024-05-13T19:17追記
ここから話広がりそうなんだけど、なんか終わってんだよねぇ。ブログのタイムスタンプによると最終更新が 2020-06-26 23:25:56
らしい。
なるほど。捨てたつもりが捨てきれてなかっただけっぽい。下書きに残してるなんて未練がましいので晒してやる。
いつか気が向いたら書き直すよ。いつか。
2021年のJavaとnullの話
ブログの下書きを眺めてたら長文がみつかったので供養しておきます。 TODOがDOされるかはわかりません。心の目で見てください。
Javaには null
があって、この子がいない世界がいいんですけど、いなくなったりはしてくれないので、仕方がありません。いい感じに付き合っていきましょうねーって話を書きます。関連エントリとかスライドはこの辺。
- ふつうのJavaコーディング
- 2017年のJJUG CCCのスライド。なんかいいこと書いてる。
- nullが嫌い
- 2019年、書いたのは2015年だって言い訳しながら投げたポエム。
- Optionalの取り扱いかた
- 2015年、Java8で登場した
Optional
に色々期待したけど、まぁほどほどなところに落ち着いたかなーって思ってます。
- 2015年、Java8で登場した
- nullを安易に使わない
- 2012年。やぁ10年弱前の私、いまだにこんな話してるよ?
以降は長文ですが、この手のは実務でいくつかの条件を満たした上での話になります。 なるべく書いたつもりですが、無意識に前提としているものはスコーンと抜けているでしょうし、まるっきり同じ条件でも「こちらの方がいい」という選択肢もあっていいと思います。その辺の話は実務でやりたいなーって思ってます。
本稿は「Javaの話」として書いてます。「他の言語にすればいい」とかは異なる話です。
私の基本的な考え
NullPointerException
(以降NPE)は起こるべきところで起こって良い。ダサかろうと予期しない null
が混入した際に発生する適切な例外はNPEであり、他の何かで取り繕う必要はない。
予期しない null
を null
チェックで誤魔化さない。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が発生した場合、スタックトレースには行番号しか表示されず、a
が null
なのか obj
が null
なのかわかりませんでした。そのためNPEの解析では一時変数で受けてみたり、a
や a.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で書いてるっぽいからどこかのボードに残骸はあるんだろけど……