Java | Java 標準ライブラリ:Optional と Stream の併用

Java Java
スポンサーリンク

なぜ「Optional と Stream の併用」が話題になるのか

Optional と Stream、どちらも「null を避けつつ、宣言的に書く」ための道具です。

でも実務でコードを書き始めると、こういう場面に必ず出会います。

「このメソッド、戻り値が Optional なんだけど、この結果を Stream の流れにうまく乗せたい」
「Stream の途中で Optional を返す処理を挟んだら、Stream<Optional<T>> になってしまって扱いづらい」

つまり、

Optional と Stream を「どうつなげるか」
Optional の「あるかもしれない 1 個」と、Stream の「0 個以上」をどう扱うか

ここが分かると、一気にコードの見通しがよくなります。


パターン1:Optional を「Stream に変換」して、自然に流れに混ぜる

Optional.stream() の感覚をつかむ(Java 9+)

Java 9 から、Optional に stream() メソッドが追加されました。

動きはとてもシンプルです。

Optional が値を持っている(non-empty)とき
→ その 1 要素だけを持つ Stream になる(要素数 1)

Optional が空(empty)のとき
→ 要素 0 の Stream になる

つまり、

Optional<T>#stream()
「0 個または 1 個の要素を持つ Stream<T> に変換する」メソッドです。

感覚的には、

「あるなら 1 個流す、ないなら何も流さない」

です。

例:Optional を Stream のパイプラインに自然に混ぜる

たとえば、ユーザー ID を元に Optional<User> を返すメソッドがあるとします。

import java.util.Optional;

Optional<User> findUserById(int id) {
    // 見つかれば Optional.of(user)、見つからなければ Optional.empty()
}
Java

複数の ID を List で持っていて、「存在するユーザーだけを集めたい」というパターン。

従来の書き方だと、こんな感じになりがちです。

List<Integer> ids = List.of(1, 2, 3, 4);

List<User> users = new ArrayList<>();
for (int id : ids) {
    Optional<User> opt = findUserById(id);
    opt.ifPresent(users::add);
}
Java

これを Stream+Optional.stream で書き直すと、こうなります。

import java.util.List;

List<Integer> ids = List.of(1, 2, 3, 4);

List<User> users =
        ids.stream()
           .map(id -> findUserById(id)) // Stream<Optional<User>>
           .flatMap(Optional::stream)   // Stream<User>(存在するものだけ)
           .toList();
Java

ここでのポイントは、

mapStream<Optional<User>> になっている
flatMap(Optional::stream)
値あり Optional → 1 要素の Stream
空 Optional → 空 Stream
になり、全部つなげて Stream<User> になる

つまり、「見つかった User だけが自然に流れてくる」状態になります。

やっていることは for + ifPresent と同じですが、
「ID → Optional<User> → User(存在するものだけ)」という流れが 1 本のパイプラインで表現できています。


パターン2:Stream の中で Optional を返す処理を挟んだときの「flatMap(Optional::stream)」

やらかしがちな Stream<Optional<T>> 地獄

Stream の途中で「0 または 1 件しか見つからない処理」を挟むと、
ついこういう形になります。

List<String> keywords = List.of("a", "b", "c");

Stream<Optional<Article>> tmp =
        keywords.stream()
                .map(kw -> findLatestArticleByKeyword(kw)); // Optional<Article>
Java

tmp の型は Stream<Optional<Article>> です。
そのままでは扱いにくいですよね。

欲しいのは Stream<Article>(実際に見つかった記事)です。

flatMap(Optional::stream) で「中身だけ取り出す」

そこでさっきと同じテクニックを使います。

List<Article> articles =
        keywords.stream()
                .map(kw -> findLatestArticleByKeyword(kw)) // Stream<Optional<Article>>
                .flatMap(Optional::stream)                // Stream<Article> に“平坦化”
                .toList();
Java

flatMap(Optional::stream) の一行で、

Optional<Article>
→ 値ありなら Article が 1 個流れる
→ 空なら何も流れない

となり、結果的に「見つかった記事だけの Stream」になります。

ここが、Optional と Stream を併用する際の一番おいしいポイントです。


パターン3:Optional の「中身」を map / filter してから Stream に流す

Optional を先に map / filter で整えてから stream() する

Optional 自体も mapfilter を持っています。

例えば、「設定値を Optional で受け取り、条件を満たすものだけを Stream に乗せたい」という場合。

Optional<String> maybeConfig = readConfig("timeout"); // "1000" とか null とか

List<Integer> timeouts =
        maybeConfig
            .filter(v -> v.matches("\\d+"))   // 数字だけの文字列に絞る
            .map(Integer::parseInt)          // Integer に変換
            .stream()                        // Optional<Integer> → Stream<Integer>
            .toList();
Java

空の Optional だったり、数字でない文字列だった場合は、
filter / map のあとで empty になり、stream() すると要素 0 の Stream になります。

結果として timeouts は要素 0 個か 1 個の List になります。

この「Optional 内で一通りの前処理をしてから stream()」は、
設定・入力値・パラメータ周りの処理でかなり便利です。


パターン4:Optional を「Stream と同じ感覚」で扱うための思考法

Stream と Optional は「0 個以上」と「0〜1 個」

一段抽象化して見ると、こう整理できます。

Stream<T>
…… 0 個以上の T の並び

Optional<T>
…… 0 個または 1 個の T

なので、本質的には「要素数の上限だけ違う、同じような“コンテナ”」です。

Stream の世界では、

map … 各要素を変換
filter … 条件に合うものだけ残す
flatMap … 1 要素 → 0〜複数に展開して平坦化

のような操作がありました。

Optional も、

map … 中身があれば変換する(なければ何もしない)
filter … 条件に合わなければ empty にする
flatMap … 中身を Optional に展開して平坦化

という、ほぼ同じ操作を持っています。

違いは、「Optional は最大 1 個」「Stream は何個でも」というだけ。

だから、

Optional を Stream に変えるときは「0〜1 個の Stream にする」
Stream の中で Optional を使うときは「0〜1 個をどう平坦化するか(flatMap)」

という視点を持っておくと、迷いにくくなります。


イマイチな併用パターンと、その直し方

悪い例:Optional を無理に null っぽく扱う

こういうコードはもったいないです。

List<Integer> ids = List.of(1, 2, 3);

List<User> users =
        ids.stream()
           .map(id -> findUserById(id).orElse(null)) // null を戻してしまう
           .filter(Objects::nonNull)
           .toList();
Java

せっかく Optional で来ているのに、わざわざ null に戻して filter(Objects::nonNull) している。

これを Optional.stream を使って書き直すと、こうです。

List<User> users =
        ids.stream()
           .map(this::findUserById)     // Stream<Optional<User>>
           .flatMap(Optional::stream)   // Stream<User>
           .toList();
Java

null が出てこないので、
Optional と Stream の両方の美味しいところを使えています。

悪い例:Stream の要素が Optional のまま残り続ける

List<Optional<User>> list =
        ids.stream()
           .map(this::findUserById)  // Stream<Optional<User>>
           .toList();                // List<Optional<User>>
Java

これも場合によってはアリですが、
呼び出し側で毎回 ifPresentorElse を書く手間が増えます。

「存在するユーザーだけ欲しい」なら、
最初から flatMap(Optional::stream) で Optional を“はがして”おいた方が、後続のコードが楽です。


どこまで Optional、それ以降どこから Stream にするか(設計の線引き)

一つの値だけを見るところまでが Optional、それを「たくさん扱う」なら Stream

設計としては、こんな分け方がしっくりきます。

1件だけの結果や設定値
…… Optional が向いている。map / filter / orElse などで処理する。

その「0〜1件」を、複数まとめて扱うとき(List や Stream の流れに乗せたい)
…… Optional.stream() で Stream に変換してやると、全体の流れに自然に溶け込む。

たとえば、

「DB から 1 件だけ取得する」
→ メソッドの戻り値は Optional<Entity>

「いろんな条件で複数回検索し、その結果を 1 個の一覧にまとめる」
→ 各 Optional を stream() して flatMap でつなげて Stream<Entity> にする

というように、「単発の世界(Optional)」と「集合の世界(Stream)」を分けて考えて、
必要なところで橋渡しするイメージです。


まとめ:Optional と Stream の併用を自分の中でこう整理する

Optional と Stream の併用を、初心者向けにギュッとまとめるとこうです。

  • Optional は「0〜1 個の T」、Stream は「0 個以上の T」
  • Java 9 以降、Optional に stream() が追加され、Stream に自然に混ぜ込める
  • map(... Optional<...> ...) の後に flatMap(Optional::stream) で「値があるものだけを取り出せる」
  • null を戻して filter(Objects::nonNull) するより、Optional のまま stream() で扱ったほうがきれい
  • 「単発の処理」は Optional のまま、「それをたくさん集約する処理」は Stream に任せる

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