この記事は、ワシントン大学のコンピュータ科学&エンジニアリングの教授であるMichael Ernst氏が2016年10月12日に投稿した記事 "Nothing is better than the Optional type" の全文を、ご本人の許可を得て翻訳したものです。
Optional
が「NullPointerException
の問題を解決する」という認識が誤りであることや、Optional
を使うことで発生するデメリットを紹介することで、Optional
クラスの使いどころや、さらにはOptional
を使う前に検討すべきポイントについて考える記事です。 (つまり、タイトルは釣りです)
(2016/10/18 追記)
コメントで @tadayosi さんより指摘いただいた通り、タイトルは釣りというよりは、「Optionalは最も優れた型である、と見せかけて『何もしないこと』(つまり従来通りの書き方)が最善である」という2つの意味を1つの文で表現したものと考えると、記事の内容とちゃんとマッチするようでした。単純に真逆のことを言って釣っているわけではないようですので、訂正いたします。(/追記)
僕のような、まだJava8でのちゃんとした開発経験が浅く、Optional
についても「nullを扱いやすくしてくれるクラスがあるらしい」くらいの認識しかない人にとってはとても参考になる記事だと思いましたので、翻訳を投稿させていただこうと思いました。
(逆に、すでにOptional
クラスを多くの場面で使っていて、その問題を実感している方にとっては「そんなの言われなくても知ってるよ」な内容かもしれません)
なるべく原文の流れのまま翻訳していますが、誤訳や不必要な意訳がありましたらご指摘いただければと思います。
本文
JDK 8で、空、もしくはnullではない値のどちらかを保持していることを表すOptionalというクラスが導入されました。
しかし、Optional
にはそのメリットを打ち消してしまうほどの問題がいくつもあります。Optional
はあなたのコードをより正確に、強固にするものではないのです。Optional
クラスが解決しようとしている課題が実際にあるものの、この記事ではその課題を解決するより良い方法を紹介します。すなわち、Optional
を使うよりも、Javaにおける従来通りのnullを想定した参照方法を用いる、という方法です。
Web上では、「Optional
はNullPointerException
の問題を解決するためのクラスである」という旨の議論で溢れています。しかし、これは__事実ではありません__。Optional
を使ってコードを書き換えると、以下のような影響が出ます。
-
NullPointerException
をNoSuchElementException
にすり替えます。プログラムがクラッシュすることには変わりありません。 - 今までは危険ではなかった別の問題を引き起こします。
- コードが乱雑になります。
- メモリや速度のオーバーヘッドが増加します。
NullPointerException
やNoSuchElementException
が投げられた場合、根底にあるロジック的なミスは、処理しているデータが取りうる全ての状態を想定したチェックが抜けている、ということです。そうならないためにも、チェックが漏れていないことを保証するツールの導入をオススメします。根底にある問題をプログラマが理解し、修正する助けになるからです。
(これらの指摘はJavaのOptional
に限ったものではありません。同様の問題を解決してきた他の言語は、結局問題を別の事象に置き換えただけにすぎないのです。)
Optional
クラスが常に悪だと言っているわけではありません。Optional
には、値が保持されているか曖昧なデータを扱おうとしたときに発生するコードの乱雑さを軽減するためのメソッド群が定義されているからです。しかし、それを考慮したとしてもOptional
クラスの使用は避けるべきです。
以降では、上述の論点をより詳しく説明していきます。以下がこの記事のアウトラインです。
- 投げられる例外を変えることは、欠陥を直したりコードの質を向上する何の役にも立たない
Optional
は誤用されるOptional
はコードを乱雑にするOptional
がオーバーヘッドを増加させる- チェック忘れこそがそもそもの問題である
Optional
の便利なメソッドたち- 反論
- まとめ
投げられる例外を変えることは、欠陥を直したりコードの質を向上する何の役にも立たない
x
とy
というフィールドを持った座標クラスを考えてみましょう。(以降の議論はgetter
メソッドについても同様です)
class Point { int x; int y; }
Javaにおいて、参照先はnullである可能性がありますので、参照先を操作する際は常にNullPointerException
が発生する危険性があります。例えば、以下のmyPoint.x
の部分です。
Point myPoint;
...
... myPoint.x ...
もしmyPoint
がnullで、実際の座標が参照できない場合、myPoint.x
はNullPointerException
を投げてプログラムをクラッシュさせるでしょう。
以下が、同様のコードをOptional
を使って書いた例です。
Point myPoint;
Optional<Point> myOPoint = Optional.ofNullable(myPoint);
...
... myOPoint.get().x ...
もし、myOPoint
が実際の座標を保持していない場合、myOPoint.get().x
はNoSuchElementException
を投げてプログラムをクラッシュさせます。これは元のコードから何一つ良くなってはいません。なぜなら、プログラマの目的はすべてのクラッシュを回避することであって、NullPointerException
によるクラッシュだけを回避できれば良いわけではないからです。
参照前にチェックすることで、例外やクラッシュを防ぐことが可能です。
if (myPoint != null) {
... myPoint.x ...
}
もしくは
if (myOPoint.isPresent()) {
... myOPoint.get().x ...
}
となります。
繰り返しになりますが、コードはどちらも似たようなもので、Optional
が従来の参照方法よりも勝っているとは言えません。
Optional
は誤用される
Optional
はJavaのクラスです。つまりOptional<Point>
型であるmyOPoint
はnullになり得ます。myOPoint.get()
はNullPointerException
もしくはNoSuchElementException
を投げる可能性があるのです!そのため、本当は以下のように書く必要があります。
if (myOPoint != null && myOPoint.isPresent()) {
... myOPoint.get().x ...
}
'Optional'クラスを使うことで、プログラマはnullであるOptional
, nullではないがデータを保持しないOptional
そしてnullでなくデータも保持しているOptional
、というデータの複雑なパターン分けを表現することができます。しかし、これは複雑でとても分かりづらいです。そうではなく、そのような場合分けを諦め、Optional
型の変数がnullにならないよう注意して制御することもできなくはありません。しかし、もしNullPointerException
が投げられないようなコーディングができる自信があるのであれば、そもそもOptional
なんて始めから使わないでしょう。
Optional
はラッパーです。そのため、値に依存する操作はエラーの原因になりがちです。例えば==
による比較、ハッシュ、そして同期処理などです。そのような処理は使わないように覚えておく必要があります。
詳しくは、Stuart MarksがOptionalの誤った使い方を避けるためのルールの一覧をまとめてくれています。
Optional
はコードを乱雑にする
Optional
ライブラリを利用すると、コードはより乱雑になります。
- 型の名前 :
Optional<Point>
かPoint
か - データのチェック :
myOPoint.isPresent()
かmyPoint == null
か - データの取得:
myOPoint.get().x
かmyPoint.x
か
どれも決定的な違いではありませんが、総じてOptional
は扱いづらく(cumbersome)、コードが汚くなります。
より具体的な例としては、http://www.codeproject.com/Articles/787668/Why-We-Should-Love-null で "cumbersome" を検索してみると良いでしょう。
Optional
がオーバーヘッドを増加させる
Optional
は空間的なオーバーヘッドを増加させます。Optional
は別々のオブジェクトであり、追加でメモリを消費するためです。
また、Optional
は時間的なオーバーヘッドも増加させます。データの実態に遠回りしてアクセスしなければならないためです。また、メソッドの呼び出しがJavaの効率的なnullチェックよりもコストが高いことも原因です。
チェック忘れこそがそもそもの問題である
NullPointerException
やNoSuchElementException
は、プログラマがデータを使用する前に!= null
や .isPresent()
などでのデータの存在チェックを怠ったときに発生します。
Optional
の最大のメリットはチェックのし忘れを防ぐことができることだという主張があります。それが本当なら素晴らしいことです!しかし残念ながら、その問題を起こりづらくするにはOptional
をコードの一部に適用させるだけでは不十分です。全てのコードで問題が取り除かれたことを保証する必要があるでしょう。
方法の一つとして、データにアクセスするまえにチェックすることをプログラマに強制する方法があります。(実際、分割代入やパターンマッチを要求することでチェックの強制を実現しているプログラミング言語もあります)。しかしこの方法は、すでにチェック済みのデータに対するチェックや、そもそも不要な場面でのチェックなども強制することになり、チェック自体が冗長になるという結果を生みます。(チェック例外がプログラマに対してどのように振る舞っているかをイメージすると良いでしょう。チェック例外は、プログラマが望むかどうかに関わらず必ずチェックすることを強制します)。
よりよいアプローチとしては、__チェック漏れがないことを保証する__ツールを導入することです。そしてそれは、冗長なチェックを必要とするものではありません。幸運なことに、そんなツールがあるのです!Checker FrameworkのNullness Checkerがそうです。
Nullness Checkerはコンパイル時に動作します。そして、全ての参照先を操作している箇所について、レシーバーがnullでないことを要求します。もしかしたら、すでに別の場所でチェック済みであったり、ロジック的にnullにならないことが保証されているかもしれません。Nullness Checkerには強力な分析機能があり、ソースコードの流れを追跡した上でnullになり得るかを判断します。Optional
を使う場合に比べて、Nullness Checkerは冗長なチェックや警告の数を減らすことができるのです。デフォルトでは、Nullness Checkerは参照先がnullでないことを前提としますが、@Nullable
アノテーションをつけることによって、データが空になり得る場所を明示することができます。
@Nullable Point
と書くこととOptional<Point>
と書くことは同じに見えるかもしれません。しかし、アノテーションには以下のようなメリットがあります。
-
@Nullable
アノテーションはメソッドのシグネチャやフィールドに対して指定するため、コードの乱雑さを抑えることができます。メソッドの本文には影響しません。 - 既存のJavaコードやライブラリと互換性があります。
Optional
のメソッドを呼び出したり、Optional
型と通常のデータ型の変換をするためにインターフェイスやクライアントに修正を加えるなんて必要もないのです。 - 実行時のオーバーヘッドがありません。
- コンパイル時に警告が出ることが保証されていますので、実行時エラーを回避できます。
- コードをドキュメント化することができます。
Optional
が使われていない場合、それはプログラマが使い忘れたからなのか、後方互換性のためにOptional
クラスを導入することができないからなのか、それとも本当にデータが常に存在することが保証されているからなのか、判断することができません。Nullness Checkerによる静的なチェックでは、アノテーションはコンパイル時に機械的にチェックすることができます。データがnullになり得る全ての場所で、@Nullable
アノテーションをつけることはとても有用です。 -
Optional
が有効でない、partially-initialized objects
やMap.get
を呼び出す場面でも、ソースコードからNullPointerException
が発生しないことを保証することができます。メソッドの前提条件も表現することができますので、nullになり得るデータをフィールドに持つ場合にも有効です。
Nullness Checkerは、コードの全ての場所で、Optional
のようにコードを破壊することなく、データの存在チェック漏れがないことを保証するという目的を達成することができました。
null参照に関連する全てのプログラマーのミスはOptional
を使ったとしても起こり得ます。さらにOptional
は新しいタイプの問題を引き起こす可能性があります。そのため、誰かがそのようなエラーがないことをコンパイル時にチェックするOptional Checkerのようなものを開発する必要があります。そしてそれは、ロジックのほとんどがNullness Checkerからの使い回しになるでしょう。Checker Framework Manualには、新しいチェッカーの作り方という章があります。それが完成するまでは、nullへの参照による被害を受け続けることになるでしょう。
Optional
の便利なメソッドたち
Optional
はコードを乱雑にしてしまう傾向は確かにありますが、いくつかのメソッドは乱雑さを解消してくれるものもあります。以下がその例です。
-
orElse
はデータが存在すればそれを、なければデフォルト値を返却してくれます。 -
map
は以下のパターンを抽象化します。- 値をインプットとして受け取り
- 値がnullだったらnullを返却し
- そうでなければ、値に関数を適用して結果を返却します
標準的なJavaで、自分自身で同じ機能を持つメソッドを実装することは簡単ですが、Optional
で実装されているものを使うことでいくつかの利便性を得られます。
-
Optional
はビルトインのため、自分で定義してテストする必要がありません - 多くのプログラマが理解可能な、標準的なメソッド名がつけられています
- インスタンスメソッドとして定義されているため、staticなメソッドを呼び出すよりも文法的に綺麗です。
他にもfilter
やflatMap
などのメソッドがOptional
のAPI仕様書に記載されています。これらのメソッドがあればOptional.isPresent()
やOptional.get()
を呼び出す必要性がほとんどなくなるでしょう。それはとても大きな利点です。しかし、全てを取り除くことができなければ、やはりOptional
の欠点は残り続けます。
反論
誰もがOptional
がNullPointerException
を解決するという誤解をしているわけではありません。例えば、OracleのJDKチームではそのような議論は出てきません。
一般的なプログラミングにおいて、データが存在しないというシチュエーションを限りなく減らすことがルールになっています。もしそれができれば、Optional<Point>
や@Nullable Point
といった記述をする必要がなくなります。しかしこの記事で指摘してきた通り、プログラムのどのような場所でも、データが存在しないということは起こりえます。
何人かの 人が 提案して いる通り、Optional
は慎重に扱うべきです。メソッドの戻り値としてのみ使い、フィールドとしては使わない、などです。Optional
の使用頻度が下がれば、プログラムの乱雑さが解消され、オーバーヘッドは減り、潜在的な誤用が減るでしょう。しかし、Optional
の問題を確実に取り除く方法はOptional
を使わない、ということだけです。さらには、Optional
の使用を減らすことは、そのメリットも減らすことになります。NullPointerException
はソースや文法構造に限らず重要です。一番の解決策は、「全て」の参照を制御することです。「一部」ではありません。
もし、今直面している課題を解決する方法がOptional
であり、それがコストに見合うのであれば、それは良いことです。使えば良いでしょう。
まとめ
まとめると、Javaの従来の参照先のチェックはOptional
のような特殊なライブラリを使うよりも以下の点で優れています。
- nullの参照で起こり得るエラーに対処することは、
Optional
を使っても同じです。そして、結果(つまり実行時エラー)は同じように発生します。つまり、Optional
単体では何も問題は解決されません。 -
Optional
は新たな潜在的な問題を引き起こします。 -
Optional
を使った構文は従来のnull参照のチェックよりも汚くなります。 -
Optional
は従来のnull参照のチェックよりも非効率的です。 -
Optional
を使う動機は、チェックすることを忘れないようにすることです。Optional
自体はそれ(チェックしているかどうか)を保証しませんが、それを保証するツールがあります。Nullness Checkerなどがそうで、それらはより強力で正確である上、コンパイル時に警告を出すことを保証してくれます。
この記事の初稿にコメントしてくれたStuart Marksに、感謝します。
Michael Ernst