ねらいと前提
値が「あるかもしれない/ないかもしれない」場面で、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(); // 無ければ例外
JavaorElse と 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、あれば判定
JavaOptional.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>
}
JavaOptional を返すことで、上位の処理系は 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"));
JavaOptional をコレクションに入れない
コレクションの中身は「値そのもの」にし、無い値は最初から流さない設計(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();
}
Java0件または1件の処理を自然に書く
opt.stream()
.map(this::transform)
.filter(this::valid)
.findFirst()
.orElseGet(this::defaultValue);
JavaOptional チェーンの基本形
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 を入れない。この作法を身につければ、値の有無ハンドリングが驚くほど読みやすく、安全に変わります。
