JavaSE8で追加されたjava.util.Optional
にはnull
との戦いに終止符を打ってもらいたいと思っているんですが、思ってるだけだと何も起こらないので、使い方とか思ったこととかを一通り書いておきます。
なお、一通りと言いつつOptionalInt
とかはスルーしています。機会と書くことがあればそのうち書くかもしれません。
Optional
については諸事情(遅筆とか理解不足とか分量とか)によりJavaエンジニア養成読本では軽い紹介にとどまっておりましたので、補足としてお読みいただけると幸いです。あと、この辺も参考にどうぞ。
Optionalのファクトリメソッド
Optional
のコンストラクタは公開されていないため、staticファクトリメソッドを使うことになります。of
, empty
, ofNullable
の3メソッドだけなので覚えましょう。全部ofNullable
で賄えはするのですが、明示的に使い分ける方がよいです。
「Optional
を作る時は常にofNullable
を使う」なんて変な癖をつけないようにしてください。null
にならないことがわかっているならば、of
を使用しましょう。想定外のnull
にofNullable
を使用してしまうと、現行では処理時にぬるぽで落ちて助かっていたものが正常として処理されてしまう可能性が高くなります。(Optional
を要求すると言うことは、値が入っていない可能性を考慮することを処理時に強制されるわけですから。)フェイルファストの意味でも、可能な場合は積極的にof
を使った方が良いでしょう。
Optional.empty()
は言うまでもなく、Optional.ofNullable(null)
と書くよりも明示的なので使わない理由はないですね。
Optionalのインスタンスメソッド
Optional
に定義されているメソッドは15個、うち3個は前述のファクトリメソッドで、さらに3個はObject
からのいつものtoString
, hashCode
, equals
です。そのため確認するのは、残りの9個になります。
9個をまとめて相手にしてもよいのですが、「マジカルナンバー7±2」を考えると、まとめて相手にするのは少し骨が折れます。分けましょう。いろんな分けかたが出来ると思いますが、7±2を考慮するならば、5個を超える分け方はできません。ということで、以下のように分けてみました。
値を取得するもの
get
, orElse
, orElseGet
, orElseThrow
の4つです。Optional<T>
に対して、T
を返す子たちです。この子たちは基本的には単純に「持っている値を返す」だけです。値を持たない場合の挙動が違うので、そこだけ押さえておきましょう。
メソッド | 値がない時 |
---|---|
get() | NoSuchElementExceptionをスロー |
orElseThrow(Supplier<Throwable>) | 引数のSupplierの戻り値をスロー |
orElse(T) | 引数のインスタンス |
orElseGet(Supplier<T>) | 引数のSupplierの戻り値 |
こうしてみると、get
はorElseThrow
の引数の要らない簡便な実装っぽく見えます。私がget
を実装するならこう書きかねないなーと思いました。
public T get() { return orElseThrow(NoSuchElementException::new); }
さすがにorElse
の実装をこうしようとは思いませんが……いや、やりかねない……。
public T orElse(T other) { return orElseGet(() -> other); }
実際はもっと素直な実装になってます。たぶんいろんな理由があるんでしょう。
気を取り直して、個別に使い方を見ていきましょう。
get, orElseThrow
get
を使用する機会は少ないはずです。Optional
をわざわざ使用すると言うことは「値が入っていない場合はよしなにしてくれ」というメッセージです。get
を使用していきなり例外になるのなら、NullPointerException
が発生するのと同じです。そして多くの現場がそうであるように「ぬるぽは単純なミス」と思われているならば、なおさら避けるべきでしょう。(この辺りについては別エントリで書きたいところです。)ぬるぽがNoSuchElementException
に変わったところで何も変わりません。今までnull
チェックの中で明示的な例外を投げていたのと同様にorElseThrow
を使用して もっと禍々しい例外 を投げましょう。
おそらく、値を取得しようとすると実行時に例外となるget
やorElseThrow
の出番はない方が健全です。これらが出現したならば、「使用するべきでない場面でOptional
が使用されているのではないか」という警告と捉えることができます。プロダクトの健康のために、これらのメソッドの出現回数をカウントしておいても良いかもしれません。
引数Optional<T>
に対してget
やorElseThrow
を使用したいのならば、素直にT
を引数型にしましょう。実行時に例外になるよりもコンパイル時に検出できた方が良いです。もちろんnull
を使用されると本末転倒なので、@NotNull
とかも使いたいところです。
なにがしかのメソッドの戻り値がOptional
ならば使うこともあるでしょうが、もし取得できない場合に例外をスローするという用途がプロダクトにおいて限定的でないならば、そのメソッドはOptional
を返さず、返す値がない場合に例外をスローしたほうが良いでしょう。全ての呼び出し箇所で判を押したようにorElseThrow
を使用する、なんてことはしてはいけません。
orElse, orElseGet
これらは値が取得できない場合のデフォルト値を指定する取得方法と見ることができます。デフォルト値を指定するAPIは他にもProperties#getProperty
などもあるので、使い方は想像がつくかもしれません。
今まで引数のドキュメントコメントに「指定できない時はnull
にしてね!」とか書いていたところは、Optional
にすることで「指定できない時はOptional.empty()
にしてね!」と伝わります。誤解のないAPI、ドキュメントコメントを読む必要がなくなる。いいかもしれない。でもわざわざOptional.empty()
とか作りたくないので、オプション引数はオーバーロードにして欲しいところですが、状況次第ではあると思います。
なおorElseGet
の引数であるSupplier
は空のOptional
に対してorElseGet
が呼ばれない限り実行されません。そのためインスタンス生成が重い場合の使用が想定されますが、デフォルト値ならば共有できるインスタンスになることも多いので出番は少ないかもしれません。
ところで、私の趣味に合わない実装の一つである 変数への再代入 ですが、その典型的な使用例である「null
でない場合だけ値を上書きする」がorElse
などにより少し見れたものになります。これだけでもOptional
を使いたくなります。
void before() { Object value = DEFAULT_VALUE; Object result = method1(); if (result != null) { value = result; } // value を使用する処理…… } void after() { Object value = method2().orElse(DEFAULT_VALUE); // value を使用する処理…… }
もちろん呼び出すメソッドがOptional
を返すようになってこそです。ここでnull
を返しうるメソッドをOptional.ofNullable(method1())
という形で呼ぶと嬉しさは激減します。駄目な実装を誤魔化すために使用するのは避けるべきです。仮にしたくなったとしても、その部分だけ別メソッドにするなど一層挟んでアダプタにするとかする方が良いです。
orElseThrow
の場合は「呼び出されるメソッドで例外をスローした方が良い」と前述しましたが、orElse
の場合も同様のことが言えるでしょう。しかしながら私の経験になりますが、適切なデフォルト値がある場合は元々その値を返却していることが多く、null
を返す場合は異常を示すことが多いと言えます。そのような場合、わざわざOptional
にする理由はありません。今まで通り適切なnull
以外の値を返しておいてください。
値取得のやり方と使いどころまとめ
メソッド | 引数に対して | 返り値に対して |
---|---|---|
get() orElseThrow(Supplier<Throwable>) |
×: 引数型をTにする | △: 常用するなら値を返す前に例外を投げさせたい |
orElse(T) orElseGet(Supplier<T>) |
△: 引数を減らしたオーバーロードを優先したい | ○: デフォルト値で処理するかifPresent で無視するかの選択になる |
値を使用するもの
ifPresent
, isPresent
の2メソッド、名前は似てるのですが使う目的は違います。グルーピングとしては合ってない気がしますが、他にうまい分けかたが思いつかなかっただけなので気にしないでください。
ifPresent
Optional
を使用する場合の本命である ifPresent
です。このメソッドには、値が存在する場合だけ行う処理をConsumer
型の引数で与えます。ポイントは以下の2点。
- 値は呼び出し側に返されず、引数の処理に渡される。
- 値が存在しない時は何も実行されない。
void before() { Object value = method1(); if (value != null) { // value を使用する処理…… } } void after() { method2().ifPresent(value -> { // value を使用する処理…… }); }
たいして短くもわかりやすくもなっていないように見えますし、なんとなく});
に古きJava臭を感じますが、これは1行の形で書くこともできますし、メソッド参照を使ってこそのLambdaだとも思うので、きっとなんとかなります。
挙げられる利点は、忘れる可能性のあったnull
チェックに対し、Optional
を処理するときは取得できない場合の振る舞いを意識させられる点です。
また、ifPresent
を使用すると必然的に値のスコープが小さくなります。"暗黙的final"によりLambda内から外側の変数への代入はできないので、わざわざ「変数のスコープは小さくしましょう!」と言わなくて良くなるのは嬉しい。
対して注意点は、空のOptional
の場合に軽くスルーされてしまうことです。本当になんの処理もしなくて良いかの検討は必須です。例えば「Optional
はifPresent
で処理する」なんて謎のルールを作ってしまうと、「なぜ処理が実行されなかったかわからない。値が入っていないなんて思わなかった。」などといったよくわからない発言を聞くことになるでしょう。
isPresent
「使ったら負け」とまで言われるisPresent
を見てみましょうか。
もちろん「null
チェックの代わりの使用」は論外です。例えば、次のような実装は、文字数と同時に分かりづらさとメンテナンスコストを増加させる悪魔の所業です。
void before() { Object value = method1(); if (value != null) { // value を使用する処理…… } } void after() { Optional<Object> value = method2(); if (value.isPresent()) { // value を使用する処理…… } }
また、Optional
を使用する意味を考えれば、使用する瞬間ですら値の有無を気にしないのが正道ではあります。そのため、isPresent
を使用して次のようにget
をするならOptional
に存在価値はありません。
Optional<Object> opt = method(); if (opt.isPresent()) { Object value = opt.get(); // value を使用する処理…… }
これがしたいのならばifPresent
を使用してください。
isPresentの使い所を考えて見る
しかしながらisPresent
がなければ、値を取得せずに値の有無が判断することができません。つまり次のようなことをするために、一度"値なし"を表すインスタンスにorElse
で変換しなきゃいけないことになります。
// 値のあるものだけ後の処理をしたい Stream.of(Optional.empty(), Optional.of("hoge"), Optional.ofNullable(null), Optional.of("fuga"), Optional.ofNullable("piyo")) .filter(Optional::isPresent) .map(Optional::get) .forEach(System.out::println);
もちろん上記程度ならばこれでいいんですが。
Stream.of(Optional.empty(), Optional.of("hoge"), Optional.ofNullable(null), Optional.of("fuga"), Optional.ofNullable("piyo")) .forEach(opt -> opt.ifPresent(System.out::println));
Optional
を単体扱う場合にisPresent
の出番は確かに無さそうなのですが、Stream
を使う場合には出てきそうに思ったので書いてみました。他の用途は思いつきません。
脱線
ものすごくどうでもいいのですが、isPresent
をmap
とorElse
で実装するとこんな感じになります。(もしやったとしてもmap
がisPresent
使ってるのでStackOverflowError
になる。あ、isPresent
使ってるところあるじゃん。)
public boolean isPresent() { return map(v -> true).orElse(false); }
orElse
の値を返してる通り、この文脈だとisPresent
も「値を取得するもの」に分類して良くなりそうです。
Optionalのまま扱うもの
ここまででObject
由来の3メソッドを除くOptional
の12メソッドのうち9メソッドを確認しましたので、3/4を掌握したことになります。これでも戦うことはできるでしょう。
しかし今の状態では、Optional
の存在しない古き良き世界とOptional
の存在する世界を出入りする方法を知っただけです。これでは何かするたびに世界を往復しなくてはいけません。そこでOptional
をOptional
のまま取り扱うメソッドの出番となります。
なお、ここまではそうでもありませんでしたが、ここからはある程度Lambdaの知識が必要になります。一方で、StreamAPIに既に馴染んでいるならばだいたいは予想がつくでしょう。
map
Javaの世界で「マップ」と言うとHashMap
などが思い浮かべられてしまうのですが、map
メソッドはマッピング、つまり対応付けです。Map
インタフェース自体もそれのはずなのですけど、どうしてもキーバリューのコンテナみたいなイメージが強いらしく、map
メソッドなどとうまくリンクしない場合があるようなので、念のため意味の再確認をしておきました。
さて、map
は引数のFunction
に従って値を変更した新しいOptional
インスタンスを作成します。例えば文字列を全部大文字にしたいならこのようにできます。
Optional<String> str = Optional.of("hoge") .map(String::toUpperCase); assert str.get().equals("HOGE");
マッピング前後の型に関連は無いので、全く違う型にすることもできます。
Optional<Integer> length = Optional.of("hoge") .map(String::length); assert length.get().equals(4);
Optional
を使用しているので、空の場合は当然何も行われませんし、null
となるマッピングが行われた場合は空のOptional
になります。(内部ではofNullable
が使われています。)
flatMap
flatMap
は「Optional
を返すFunction
を渡してもOptional
がネストされないmap
」です。何を言っているのかわからないと思いますが、私も何を言っているのかわかりませんので、コードで理解してください。
Optional<Object> result = Optional.of("hoge") .flatMap(value -> Optional.of(value + "fuga")) .flatMap(Optional::of) .flatMap(value -> Optional.of(value + "piyo")); assertThat(result.toString(), is("Optional[hogefugapiyo]")); // 単にmapに Function<String, Optional<String>> を渡したら素敵なことになる。 Optional<Object> result2 = Optional.of("hoge") .map(value -> Optional.of(value + "fuga")) .map(Optional::of) .map(value -> Optional.of(value + "piyo")); assertThat(result2.toString(), is("Optional[Optional[Optional[Optional[hogefuga]]piyo]]"));
flatMap
は「Optional
を返すメソッドがある程度作られてから」が本番です。レベルが低いと役に立たない武器とか魔法とかはRPGではよくありますが、それと同様にプロダクトや使用ライブラリのOptional
レベル(なにそれ)がある程度上がってくると出番が増えてきます。
filter
値を受け取ってboolean
を返すPredicate
を引数にとり、条件に合致しないものを空のOptional
にする子です。挙動もわかりやすいですし、あまり説明することはありません。
使用例を捻出すると、現場ではString
のnull
と空文字列""
を同じ扱いとする場合に、空のOptional
に揃えると言った使い方が想定されます。
Predicate<String> isEmpty = String::isEmpty; Optional<String> opt = Optional.ofNullable(str) .filter(isEmpty.negate());
メソッド参照のPredicate
をうまく反転させる方法が欲しいものだ……
これまでの流れに乗って一応書いておきますと、「条件に合致しない場合はnull
にマッピングする」と捉えればmap
で代替可能です。例えばfilter
をこう実装することもできそう。
public Optional<T> filter(Predicate<? super T> predicate) { return map(v -> predicate.test(v) ? v : null); }
なお、filter
やmap
はisPresent
のときにさらっと登場させてたりします。引っかかってしまってたらごめんなさい。
まとめ
- 生成は
ofNullable
でだいたい賄えるけど、3つとも使い分けよう。 - なんでもかんでも
Optional
にしない。get
,orElseThrow
はなるべく避けたい。orElse
,orElseGet
は引数に使いたくない。
- 大抵は
orElse
かifPresent
のどちらで処理するかを選ぶ。 isPresent
は要らない子ではない。map
やfilter
が使えないと変換地獄になる。
Optional
は文字通りに「任意」や「選択」を意味する物であり、使用者はどうするかを意識して選択する必要があります。また、選択して欲しいと言う意図をコードに込めやすくなりました。Optional
を要求する/要求されることがどのような意味を持つかを意識しながら使っていけば、変な方には行かないんじゃ無いかなと思います。