概要
先日参加した Devoxx US 2017 というカンファレンスで、Optional を扱う際のルールについて知ったので、まとめるついでに思ったことを書いてみます。
Optional の7つのルール
Mr.Stuart Marks(@stuartmarks) の "Optional - The Mother of all Bikesheds" というセッションを聴講しました。そこで java.util.Optional を使うときの7つのルールが紹介されていました。
- Never, ever, use null for an Optional variable or return value.
- Never use
Optional.get()
unless you can prove that the Optional is present. - Prefer alternatives to
Optioal.isPresent()
andOptional.get()
. - It’s generally a bad idea to create an Optional for the specific purpose of chaining methods from it to get a value.
- If an Optional chain has a nested Optional chain, or has an intermediate result of Optional, it’s probably too complex.
- Avoid using Optional in fields, method parameters, and collections.On a related note, I thought of another rule after I presented the session:
- Don’t use an Optional to wrap any collection type (List, Set, Map). Instead, use an empty collection to represent the absence of values.
それぞれがどういうことかは(Rule 7を除いて)後述の資料に記載がありますので、そちらをご覧ください。ちなみに present は「Optional に値が入っている状態(≒Not Empty)」、absent は「Optional に値が入っていない状態(≒Empty)」を示します。
資料
まだ Devoxx US 2017 版のは公開されていませんが、その前にも別のカンファレンスで同じタイトルのセッションをなさっていたようで、その資料と映像は公開されています。この資料、Optional を使い始めようと考えている人、最近 Optional を使い始めた人には非常に有益な情報が載っていますので、一読をお勧めします。
Optional の orElse Family
Optional に入っている値を取り出すには get() か orElse Family メソッドを使います。get() メソッドは Optional オブジェクトが empty のときに実行時例外 NoSuchElementException が出ますので、 isPresent で確認しない限りは使ってはいけないとのことです。通常は orElse Family メソッド、ないし ifPresent(present の時に実施する処理を Lambda 式で記述するメソッド) を使うことになるでしょう。
Method | Do absent |
---|---|
orElse(DEFAULT_VALUE) | DEFAULT_VALUE を返す(もしここにメソッドを指定した場合、 present でも absent でも実行される) |
orElseGet(Object::new) | Supplier.get が呼ばれ、そこで生成されたオブジェクトが返される |
orElseThrow(Exception::new) | 例外を投げる |
積極的に Optional.orElseGet を使いたいケース
ちょっと前に TL で話題になっていました。(参考:Optionalから値を取り出すにはorElseGet()を使う) absent な時に返す値をメソッド呼び出しで用意する場合は、積極的に orElseGet を使った方がよいでしょう。こちらだと absent の時だけメソッドが呼ばれます。
orElse
.orElse(Collections.emptyList())
orElseGet
.orElseGet(Collections::emptyList)
Optional.get()
これまで「Optional.get()
は Optional の価値を否定するものだから削除すべき」と個人的に思っていました。が、実際はそう簡単な話でもないとわかりました。まず下記のコードをご覧ください。
final Optional<Bounds> caretBoundsOr = editor.getCaretBounds();
if (caretBoundsOr.isPresent()) {
final Bounds bounds = caretBoundsOr.get();
return new Point2D(bounds.getMinX(), bounds.getMinY() + 20);
}
return null;
「isPresent でチェックしてから get する」というルールに則った処理です。 absent のケースでは return null;
しているのが、「null 返すなら何で Optional 使っているの」と突っ込みたくなるケースです。
では、何で使ったのか?
- ライブラリの API が Optional でない生の値を返すメソッドを用意していなかった…… RichTextFX というライブラリの CodeArea.getCaretBounds メソッドが Optional を返してくるので、使わざるを得ませんでした。
- オブジェクト生成がライブラリ使用者からは呼び出しにくい設計となっていたため、 orElse Family のメソッドを使えなかった
前述の通り、一応 isPresent() でチェックしてから値を取り出しているので、Mr.Marks のルールには則っています。が、Optional の isPresent -> get はただの null チェックをするコードよりも良いとは言い難いです。
ただの null チェックをするコードに Optional が劣る点
- Optional がオブジェクトであり、単純な null チェックでは使う必要のない Optional オブジェクトを1つ余計に生成している
- Optional を使ったコードにすると、デバッガでの細かいステップ実行ができなくなる
- Java 8 以降でないと Optional が利用できない
修正案
考えてみたところ、無難なのはこんな書き方でしょうか……
final Optional<Point2D> pointOr
= editor.getCaretBounds()
.map(bounds -> new Point2D(bounds.getMinX(), bounds.getMinY() + 20));
return pointOr.isPresent() ? pointOr.get() : null;
Optional は null チェックだけでなく、その後の処理も一緒に書くようなケースで filter(値が条件に一致する場合のみ次の operator に処理を続ける) や map(値を変換する) と一緒に用いた方が良いのではないかと思います。
安易な Optional の活用よりも三項演算子& null チェックが適するケース
Mr.Stuart Marks は下記のような Optional を使ったコードを書くなら、シンプルに三項演算子を使え、ということを仰りたかったのではないかと思いました。
Optional.ofNullable(runScript()).orElse("0").toString()
三項演算子を使った方が短い上にわかりやすく、そして無駄な Optional オブジェクトも作らなくて済みます。
final String script = runScript();
script != null ? script : "0"