ブログの下書きを眺めてたら長文がみつかったので供養しておきます。
TODOがDOされるかはわかりません。心の目で見てください。
Javaには null
があって、この子がいない世界がいいんですけど、いなくなったりはしてくれないので、仕方がありません。いい感じに付き合っていきましょうねーって話を書きます。関連エントリとかスライドはこの辺。
以降は長文ですが、この手のは実務でいくつかの条件を満たした上での話になります。
なるべく書いたつもりですが、無意識に前提としているものはスコーンと抜けているでしょうし、まるっきり同じ条件でも「こちらの方がいい」という選択肢もあっていいと思います。その辺の話は実務でやりたいなーって思ってます。
本稿は「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で書いてるっぽいからどこかのボードに残骸はあるんだろけど……