Optionalの見直し – Java9で追加されていたメソッドが便利すぎることに今更気づいた

| 7 min read
Author: toshio-ogiwara toshio-ogiwaraの画像

OptionalクラスはJava8で追加された当初はStream APIやラムダと同じように大いに話題になり、ネットで取り上げられることも多かったですが、その後はOptionalクラスがJavaの標準APIとして定着するに従い、注目されることもなくなっていきました。そんなOptionalクラスですが地味にJava9で便利なメソッドが追加され進化していることに(今さら)気がつきました。

ということで、今回はあまりネットにはないJava9でOptionalに追加されたメソッドの便利な使い方を自分の備忘兼ねて紹介したいと思います。

or - フォールバック処理が断然見やすくなる

#

プログラムでは「ある処理が期待どおりでなかった場合は別の処理を行い、その処理も期待どおりでなかった場合はさらに別の処理を行う」といったフォールバック処理を書くことがよくあります。

旧来はこのような処理はif文で分岐しながら記述する必要がありましたが、Java9で追加されたOptional#orメソッドを使うことで分岐を使わずスッキリと処理を記述できるようになりました。

例えば「ファイル、classpathリソース、URLの順で引数で渡されたパスの解決を試み、最初に解決できたパスから値を取得する」といった処理があった場合、旧来はif文で次のように処理を記述する必要がありました。

  • if文を使った旧来の例
public String resolveValue(String path, String defaultValue) {
Optional<String> val = fromFilePath(path);
if (val.isPresent()) {
return val.get();
}
val = fromResourcePath(path);
if (val.isPresent()) {
return val.get();
}
val = fromUrlPath(path);
if (val.isPresent()) {
return val.get();
}
return defaultValue;
}
private Optional<String> fromFilePath(String path) {
return path.startsWith("file:")
? Optional.of("file value")
: Optional.empty();
}
private Optional<String> fromResourcePath(String path) {
return path.startsWith("jar:")
? Optional.of("jar value")
: Optional.empty();
}
private Optional<String> fromUrlPath(String path) {
return path.startsWith("http:")
? Optional.of("url value")
: Optional.empty();
}
// --- 利用コードイメージ
var val1 = resolveValue("jar:file:/foo/bar/target", "unknown value");
var val2 = resolveValue("net:/foo/bar/target", "unknown value");

別のやり方としてif文を使わずすごく頑張ってOptionalを使って記述した場合、次のようなorElseGetメソッドが入れ子になったパズルのような実装になります。

  • Optional#orElseGetメソッドで頑張り過ぎた例
public String resolveValue(String path, String defaultValue) {
return fromFilePath(path)
.orElseGet(() -> fromResourcePath(path)
.orElseGet(() -> fromUrlPath(path)
.orElse(defaultValue))

);
}
...

いずれもイマイチでしたが、これをOptional#orメソッドを使って書くと次のようにスッキリと記述することができます。(気持ちイイぃー

  • Optional#orメソッドでスッキリした例
public String resolveValue(String path, String defaultValue) {
return fromFilePath(path)
.or(() -> fromResourcePath(path))
.or(() -> fromUrlPath(path))
.orElse(defaultValue);
}
...

orElseGetメソッドとorメソッドは一見同じようなことをしているように見えますが戻り値が異なります。orElseGetメソッドの戻り値は型パラメータのT、例の場合はStringとなるため、returnする時点で戻す値を決定する必要があるのに対して、orメソッドはOptionalを返せるため「値がなかったら」の分岐をメソッドチェーンで繋げていくことができるようになっています。

これはすごく便利ですので、Java9以上を使っている人は必須で覚えるべきコーディングスタイルといえるでしょう。

ifPresentOrElse – 値がなかった場合の処理も書ける

#

Java8でもOptionalの値がなかった場合のConsumer処理(値を戻さない処理)をifPresentメソッドで記述することができましたが「なかったら」に相当するelse句はOptionalのメソッドで書くことができませんでした。

このため「値があったらその値をコンソールに出力し、値がなかった場合は固定の文字列を出力する」といった処理が合った場合、次の例のようにif文を使って記述する必要がありました。

  • if文を使った例
public void outputValue(String path) {
Optional<String> value = fromFilePath(path);
if (value.isPresent()) {
System.out.println(value.get());
} else {
System.out.println("unknown value");
}
}
...

これもイマイチといいますか、ifPresentメソッドでthen句の相当する処理が書けるなら、else句も同じように書きたいなぁ~と思っていたところ、Java9で追加されたifPresentOrElseメソッドで次のように記述できるようになっていました。

  • ifPresentOrElseメソッドを使った例
public void outputValue(String path) {
fromFilePath(path).ifPresentOrElse(
System.out::println, // then句の処理
() -> System.out.println("unknown value")); // else句の処理
}
...

if文とifPresentOrElseメソッドを使った例を見比べると「if文の方が直観的でいい気がするんですが・・」という声が聞こえてきそうですが、筆者もif文の方が良いかなぁ~と若干思ったりもします。

とは言うもののthen句とelse句に記述する処理が短ければ、やはりifPresentOrElseメソッドの方がスッキリしていると思いますし、なによりifPresentOrElseメソッドではラムダが使えるので例のようにメソッド参照を使うことで少しかっこよく書くことができます。

isEmpty – 地味だがメソッド参照で威力を発揮

#

Optionalには「値が存在するか」を調べるisPresentメソッドは当初からあったため、その反対の「値が存在しないこと」を調べることについても困ることはありませんでした。しかし、メソッドの対称性やListやMapなどのメソッドとの一貫性からそもそも論としてisEmptyメソッドもあった方がベターでした。ただ、それ以上にStream APIで存在しなかった場合に否定条件が簡潔に書けなくなるのが個人的にイマイチでした。

例えばリスト要素に含まれる空要素をカウントしようとした場合、次のようにラムダ式で書く必要がありました。

  • isPresentメソッドを使った例
List<Optional<String>> optList = List.of(Optional.of("1"), Optional.empty(), Optional.of("3"));
long nullCount = optList.stream()
.filter(v -> !v.isPresent()) // ラムダで否定条件を記述
.count();

このコードになんの不満があるの?と思う方もいるかも知れませんが、このfilterメソッドの条件が「存在するか?」の肯定条件だった場合、Optional::isPresentとメソッド参照で簡潔に書くことができます。これを否定形にするだけで冗長な記述にならざるを得ないことに筆者は不満でした。

と思っていましたが、Java9から追加されたisEmptyメソッドを使うことで次のようにメソッド参照を使ってスッキリ記述することができるようになりました。

  • isEmptyメソッドを使った例
List<Optional<String>> optList = List.of(Optional.of("1"), Optional.empty(), Optional.of("3"));
long nullCount = optList.stream()
.filter(Optional::isEmpty) // メソッド参照で記述可能
.count();

ちなみにStream#filterメソッドで否定条件を使うと記述が冗長になることはOptionalクラス以外でもよく起きていたため、その対処としてJava11からPredicate.notメソッドが追加され、この問題は全体的に緩和されています。参考までに先ほどの例をPredicate.notメソッドを使って書き直すと次のようになります

  • Predicate.notメソッドを使った例
List<Optional<String>> optList = List.of(Optional.of("1"), Optional.empty(), Optional.of("3"));
long nullCount = optList.stream()
.filter(Predicate.not(Optional::isPresent)) // notメソッドの利用
.count();

orThrow – 例外送出をショートカット

#

値がなかった場合にOptionalで例外を送出するには次のようにorThrowメソッドの引数に例外の送出処理を記述する必要がありました。

  • orThrowメソッドで送出処理を渡す例
public String getValue2(String path) {
return fromFilePath(path)
.orElseThrow(IllegalArgumentException::new); // 例外送出(コントラクタ参照)
}

値がなかった場合に行う例外送出は捕捉処理で必要となる情報を設定するといったことは余りなく、単に処理を中断したいだけということが往々にしてあります。このような場合にもJava8までは上記例のようになんらかの例外送出処理を記述する必要がありました。

これに対してJava9から追加された引数なしorThrowメソッドを使うことで次のように簡潔に例外を送出させることができます。

  • 引数なしorThrowメソッドで送出処理を渡す例
public String getValue(String path) {
return fromFilePath(path).orElseThrow(); // 引数不要
}
// 送出例外のスタックトレース
Exception in thread "main" java.util.NoSuchElementException: No value present
at java.base/java.util.Optional.orElseThrow(Optional.java:377)
...

ただしこれは単に処理を中断させたいだけの場合は使うことができますが、エラー原因が分かりやすいように引数の情報を例外メッセージに設定するといった例外個別の処理を行いたい場合には使うことができません。このような場合は従来どおり次のようにorThrowメソッドに例外の送出処理を記述する必要があります。

  • orThrowメソッドで例外メッセージを設定する場合の例
public String getValue2(String path) {
return fromFilePath(path)
.orElseThrow(() -> new IllegalArgumentException("path=" + path)); // メッセージを設定
}

まとめ

#

OptionalやStreamを使っているとifを見ると消したくなるといった衝動にかられ、むやみやたらにOptionalやStreamでなんとかしたくなりますが、大事なのはスッキリしているか?エレガントか?です。Optionalを頑張って使ってif文をなくしたからといって常にエレガントになるとは限りません。かえって不細工になることもあります。ですので、用法、用量に気を付けて使いましょう!

豆蔵デベロッパーサイト - 先週のアクセスランキング
  1. 基本から理解するJWTとJWT認証の仕組み (2022-12-08)
  2. Docker+Wasm で WASM をコンテナとして実行する (2023-01-25)
  3. 自然言語処理初心者が「GPT2-japanese」で遊んでみた (2022-07-08)
  4. 直感が理性に大反抗!「モンティ・ホール問題」 (2022-07-04)
  5. Nuxt3入門(第4回) - Nuxtのルーティングを理解する (2022-10-09)
  6. AWS認定資格を12個すべて取得したので勉強したことなどをまとめます (2022-12-12)
  7. Jest再入門 - 関数・モジュールモック編 (2022-07-03)
  8. ORマッパーのTypeORMをTypeScriptで使う (2022-07-27)
  9. Nuxt3入門(第8回) - Nuxt3のuseStateでコンポーネント間で状態を共有する (2022-10-28)
  10. Nuxt3入門(第1回) - Nuxtがサポートするレンダリングモードを理解する (2022-09-25)