読者です 読者をやめる 読者になる 読者になる

日々常々

ふつうのプログラマがあたりまえにしたいこと。

Optionalの取り扱いかた

Java8 Java入門+α

JavaSE8で追加されたjava.util.Optionalにはnullとの戦いに終止符を打ってもらいたいと思っているんですが、思ってるだけだと何も起こらないので、使い方とか思ったこととかを一通り書いておきます。

なお、一通りと言いつつOptionalIntとかはスルーしています。機会と書くことがあればそのうち書くかもしれません。

Optionalについては諸事情(遅筆とか理解不足とか分量とか)によりJavaエンジニア養成読本では軽い紹介にとどまっておりましたので、補足としてお読みいただけると幸いです。あと、この辺も参考にどうぞ。

Optionalのファクトリメソッド

Optionalコンストラクタは公開されていないため、staticファクトリメソッドを使うことになります。of, empty, ofNullable の3メソッドだけなので覚えましょう。全部ofNullableで賄えはするのですが、明示的に使い分ける方がよいです。

Optionalを作る時は常にofNullableを使う」なんて変な癖をつけないようにしてください。nullにならないことがわかっているならば、ofを使用しましょう。想定外のnullofNullableを使用してしまうと、現行では処理時にぬるぽで落ちて助かっていたものが正常として処理されてしまう可能性が高くなります。(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の戻り値

こうしてみると、getorElseThrowの引数の要らない簡便な実装っぽく見えます。私が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を使用して もっと禍々しい例外 を投げましょう。

おそらく、値を取得しようとすると実行時に例外となるgetorElseThrowの出番はない方が健全です。これらが出現したならば、「使用するべきでない場面でOptionalが使用されているのではないか」という警告と捉えることができます。プロダクトの健康のために、これらのメソッドの出現回数をカウントしておいても良いかもしれません。

引数Optional<T>に対してgetorElseThrowを使用したいのならば、素直に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の場合に軽くスルーされてしまうことです。本当になんの処理もしなくて良いかの検討は必須です。例えば「OptionalifPresentで処理する」なんて謎のルールを作ってしまうと、「なぜ処理が実行されなかったかわからない。値が入っていないなんて思わなかった。」などといったよくわからない発言を聞くことになるでしょう。

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を使う場合には出てきそうに思ったので書いてみました。他の用途は思いつきません。

脱線

ものすごくどうでもいいのですが、isPresentmaporElseで実装するとこんな感じになります。(もしやったとしてもmapisPresent使ってるのでStackOverflowErrorになる。あ、isPresent使ってるところあるじゃん。)

public boolean isPresent() {
    return map(v -> true).orElse(false);
}

orElseの値を返してる通り、この文脈だとisPresentも「値を取得するもの」に分類して良くなりそうです。

Optionalのまま扱うもの

ここまででObject由来の3メソッドを除くOptionalの12メソッドのうち9メソッドを確認しましたので、3/4を掌握したことになります。これでも戦うことはできるでしょう。

しかし今の状態では、Optionalの存在しない古き良き世界とOptionalの存在する世界を出入りする方法を知っただけです。これでは何かするたびに世界を往復しなくてはいけません。そこでOptionalOptionalのまま取り扱うメソッドの出番となります。

なお、ここまではそうでもありませんでしたが、ここからはある程度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にする子です。挙動もわかりやすいですし、あまり説明することはありません。

使用例を捻出すると、現場ではStringnullと空文字列""を同じ扱いとする場合に、空の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);
}

なお、filtermapisPresentのときにさらっと登場させてたりします。引っかかってしまってたらごめんなさい。

まとめ

  • 生成はofNullableでだいたい賄えるけど、3つとも使い分けよう。
  • なんでもかんでもOptionalにしない。
    • get, orElseThrowはなるべく避けたい。
    • orElse, orElseGetは引数に使いたくない。
  • 大抵はorElseifPresentのどちらで処理するかを選ぶ。
  • isPresentは要らない子ではない。
  • mapfilterが使えないと変換地獄になる。

Optionalは文字通りに「任意」や「選択」を意味する物であり、使用者はどうするかを意識して選択する必要があります。また、選択して欲しいと言う意図をコードに込めやすくなりました。Optionalを要求する/要求されることがどのような意味を持つかを意識しながら使っていけば、変な方には行かないんじゃ無いかなと思います。