Java 逆引き集 | Stream と Optional の良い組み合わせ — 値の有無ハンドリング

Java Java
スポンサーリンク

ねらいと前提

値が「あるかもしれない/ないかもしれない」場面で、Optional は null 安全を保ちながら表現できます。Stream は「たくさんの値」を宣言的に流せます。この二つをうまく組み合わせるコツは、Optional を「0件または1件のストリーム」として扱うこと、そしてストリームから得られる Optional を自然に受け取って分岐を減らすことです。重要なのは flatMap(Optional::stream) を軸に、「無い値は自然に流れない」形へ整えることです。

基本の組み合わせパターン

Optional をストリームに変換する

Optional は 0件または1件の要素と見なせます。flatMap(Optional::stream) を使うと、ある値だけが流れ、無い値は自然に落ちます。複数の Optional を安全に合流する最小パターンです。

Optional<String> a = Optional.of("A");
Optional<String> b = Optional.empty();
Optional<String> c = Optional.of("C");

List<String> out = Stream.of(a, b, c)
    .flatMap(Optional::stream) // ある値だけが流れる
    .toList();                 // ["A", "C"]
Java

この感覚を持てば、Optional の明示的な isPresent チェックや get 呼び出しが不要になります。

ストリームから Optional を受け取る

findFirst、findAny、min、max は Optional を返します。結果が無くても安全に扱え、orElse、orElseGet、orElseThrow でデフォルトや例外化を管理できます。

Optional<Integer> top = Stream.of(3,1,4)
    .sorted(Comparator.reverseOrder())
    .findFirst(); // Optional[4]

int value = top.orElse(0);            // 無ければ0
int lazy = top.orElseGet(() -> 42);   // 無ければ遅延生成
int must = top.orElseThrow();         // 無ければ例外
Java

orElse と orElseGet の違いは「デフォルト値の計算タイミング」です。重い計算なら orElseGet を選び、不要な計算を避けます。

例題で理解する良い組み合わせ

例題一 件名の正規化と最初の一致を取得する

複数ソースから最初に見つかったタイトルを取りたい。Optional と Stream を組み合わせれば、分岐なしに書けます。

Optional<String> fromDb = Optional.empty();
Optional<String> fromCache = Optional.of("  hello  ");
Optional<String> fromApi = Optional.of("Hi");

Optional<String> normalized =
    Stream.of(fromDb, fromCache, fromApi)
          .flatMap(Optional::stream)           // ある値だけ
          .map(String::trim)                   // 正規化
          .filter(s -> !s.isBlank())           // 無意味な値除去
          .findFirst();                        // 最初の有効値

String title = normalized.orElse("Untitled");
Java

「無い値は流れない」「最初に有効なものだけ採用」という形が自然に表現できます。

例題二 ID のパースと存在チェックを直列化する

文字列から数値 ID を安全にパースし、コレクション内に存在するかを Optional で繋ぎます。

Optional<Integer> parseInt(String s) {
    try { return Optional.of(Integer.parseInt(s)); }
    catch (NumberFormatException e) { return Optional.empty(); }
}

List<Integer> ids = List.of(10, 20, 30);

boolean has = parseInt("20")
    .stream()                       // 0/1件のストリームへ
    .anyMatch(ids::contains);       // 無ければ false、あれば判定
Java

Optional.stream によって、「無い入力ならそのまま不一致」と自然に落ちます。余計な if が不要です。

例題三 レコードから最初の有効メールアドレスを拾う

複数フィールドの候補から、最初の妥当なメールを選ぶ定番パターンです。

record User(String primary, String secondary, String backup) {}

Optional<String> pickEmail(User u) {
    return Stream.of(u.primary(), u.secondary(), u.backup())
        .filter(Objects::nonNull)                  // null除去
        .map(String::trim)
        .filter(s -> s.contains("@"))              // 簡易妥当性
        .findFirst();                              // Optional<String>
}
Java

Optional を返すことで、上位の処理系は orElse でデフォルトしたり、orElseThrow で「無いならエラー」と明確に判断できます。

深掘りポイント

map と flatMap の違いを腹落ちさせる

Optional.map は「値があれば変換、無ければ empty のまま」です。Optional.flatMap は「変換結果も Optional」であるときに、二重 Optional を解消します。ストリームでも同様に、map は「要素ごとに1→1」、flatMap は「1→0..N」に展開します。

Optional<String> s = Optional.of("hi");
// map: Optional<String> -> Optional<Integer>
Optional<Integer> len = s.map(String::length);

// flatMap: Optional<String> -> Optional<String>(もう一段の Optional を溶かす)
Optional<String> upperOrEmpty = s.flatMap(x -> x.isBlank() ? Optional.empty()
                                                           : Optional.of(x.toUpperCase()));
Java

ストリームで Optional を合流するときは flatMap(Optional::stream)。Optional 同士を合成するときは flatMap を使うのが鉄則です。

orElse と orElseGet の選択

orElse は「常に右側を評価」します。重い計算やログ出力を含むと、値がある場合でも無駄に実行されます。orElseGet は「必要になったときだけ評価」するため、重いデフォルトは orElseGet 一択です。

String val = opt.orElseGet(() -> heavyDefault()); // 重いならこちら
Java

例外化は境界で行う

orElseThrow は「無いなら例外」。ドメイン的に「必須値」に昇格する境界(例:APIコントローラ、集計の最終段)で使い、途中のパイプラインでは Optional を保ったまま柔軟に流すと、再利用性が上がります。

String email = pickEmail(user).orElseThrow(() -> new IllegalStateException("email required"));
Java

Optional をコレクションに入れない

コレクションの中身は「値そのもの」にし、無い値は最初から流さない設計(flatMap(Optional::stream))にします。Optional<List<T>> か List<T> の Optional かを迷ったら、「無い時は空リスト」戦略を優先すると Stream と相性が良くなります。

Optional<List<String>> maybeNames = Optional.of(List.of("A","B"));
// 無い時は空とみなして安全に流す
List<String> names = maybeNames.orElseGet(List::of);
Java

実戦レシピ

複数ソースから最初の値を選ぶ

複数の Optional ソースを優先順に並べ、最初に見つかったものを取ります。flatMap(Optional::stream) と findFirst の組み合わせが定番です。

Optional<String> firstPresent(Optional<String>... opts) {
    return Stream.of(opts).flatMap(Optional::stream).findFirst();
}
Java

検索、パース、検証を直列化する

文字列入力を検証してから検索し、結果の整形まで一息に書くと分岐が消えます。

Optional<String> normalizedId = Optional.of("  100  ")
    .map(String::trim)
    .filter(s -> s.matches("\\d+"));               // 数字だけ

Optional<User> found = normalizedId
    .flatMap(s -> findUserById(Integer.parseInt(s))); // Optional<User>

String label = found
    .map(User::name)
    .map(String::toUpperCase)
    .orElse("UNKNOWN");
Java

「無いなら流れない」を徹底すると、ネストも if も消え、宣言的になります。

テンプレート

Optional の合流をストリームで

Optional<T> pickFirst(Optional<T> a, Optional<T> b, Optional<T> c) {
    return Stream.of(a, b, c).flatMap(Optional::stream).findFirst();
}
Java

0件または1件の処理を自然に書く

opt.stream()
   .map(this::transform)
   .filter(this::valid)
   .findFirst()
   .orElseGet(this::defaultValue);
Java

Optional チェーンの基本形

Optional<R> out = Optional.ofNullable(input)
    .map(this::normalize)
    .filter(this::valid)
    .flatMap(this::lookupOptional) // Optional を返す関数は flatMap
    .map(this::finalize);
Java

まとめ

Optional は「無いかもしれない」を安全に表現し、Stream は「たくさん」を宣言的に流します。両者の良い組み合わせは、Optional を 0件または1件のストリームとして扱い、flatMap(Optional::stream) で「無い値を自然に除外」すること。ストリームから得られる Optional は orElse/orElseGet/orElseThrow で境界ごとに適切に扱い、途中では Optional のまま柔軟に繋ぐ。map と flatMap の役割を腹落ちさせ、重いデフォルトは orElseGet、コレクションには Optional を入れない。この作法を身につければ、値の有無ハンドリングが驚くほど読みやすく、安全に変わります。

タイトルとURLをコピーしました