この記事は、ワシントン大学のコンピュータ科学&エンジニアリングの教授である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