- なぜ「Optional と Stream の併用」が話題になるのか
- パターン1:Optional を「Stream に変換」して、自然に流れに混ぜる
- パターン2:Stream の中で Optional を返す処理を挟んだときの「flatMap(Optional::stream)」
- パターン3:Optional の「中身」を map / filter してから Stream に流す
- パターン4:Optional を「Stream と同じ感覚」で扱うための思考法
- イマイチな併用パターンと、その直し方
- どこまで Optional、それ以降どこから Stream にするか(設計の線引き)
- まとめ:Optional と Stream の併用を自分の中でこう整理する
なぜ「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ここでのポイントは、
map で Stream<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>
Javatmp の型は 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();
JavaflatMap(Optional::stream) の一行で、
Optional<Article>
→ 値ありなら Article が 1 個流れる
→ 空なら何も流れない
となり、結果的に「見つかった記事だけの Stream」になります。
ここが、Optional と Stream を併用する際の一番おいしいポイントです。
パターン3:Optional の「中身」を map / filter してから Stream に流す
Optional を先に map / filter で整えてから stream() する
Optional 自体も map や filter を持っています。
例えば、「設定値を 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();
Javanull が出てこないので、
Optional と Stream の両方の美味しいところを使えています。
悪い例:Stream の要素が Optional のまま残り続ける
List<Optional<User>> list =
ids.stream()
.map(this::findUserById) // Stream<Optional<User>>
.toList(); // List<Optional<User>>
Javaこれも場合によってはアリですが、
呼び出し側で毎回 ifPresent や orElse を書く手間が増えます。
「存在するユーザーだけ欲しい」なら、
最初から 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 に任せる
