JavaのStream APIは、Listなどのコレクションに対して、絞り込み、変換、集計といった処理を流れとして書ける仕組みです。for文でも同じ処理は書けますが、filter、map、collectを使うことで、「何をしたい処理なのか」を読み取りやすくできる場面があります。ただし、複雑な分岐や副作用が多い処理では、無理にStream化せず、for文で素直に書く判断も重要です。

1. Stream APIとは何か
1-1. コレクション処理を流れとして書く仕組み
Stream APIは、Listなどのコレクションに対する処理を、データの流れとして書くための仕組みです。要素を1つずつ取り出して、条件で絞り込み、別の形に変換し、最後に結果をまとめるような処理に向いています。
従来のfor文では、「リストを回す」「条件を判定する」「結果用のリストへ追加する」といった手順を自分で細かく書きます。一方、Stream APIでは、filterで絞り込み、mapで変換し、collectで結果をまとめるように、処理の意味をメソッド名で表現できます。
List<String> names = users.stream()
.filter(user -> user.isActive())
.map(User::getName)
.toList();
このコードでは、有効なユーザーだけを残し、その名前を取り出してListにしています。注意点は、Stream APIは「何をしたいか」を読みやすくできる一方で、複雑な処理を無理に1つの流れに詰め込むと、逆に読みにくくなることです。
1-2. for文との違い
Stream APIとfor文の違いは、手順を細かく書くか、処理の目的を流れとして書くかです。Stream APIはfor文の完全な置き換えではありません。
for文は、途中で変数を更新したり、複雑な分岐を入れたり、デバッグ用の処理を差し込んだりしやすい書き方です。一方、Stream APIは、コレクションに対して「絞る」「変換する」「集める」といった処理を宣言的に書きやすい特徴があります。どちらが優れているかではなく、処理内容によって向き不向きがあります。
たとえば、単純な一覧変換ならStream APIが読みやすいことがあります。一方、ループの途中で複数の状態を更新する、条件によって処理の流れが大きく変わる、例外処理が多い、といった場合はfor文のほうが読みやすいこともあります。
1-3. Stream APIが向いている処理
Stream APIが向いているのは、コレクションの絞り込み、変換、集計、検索のように処理の流れがシンプルな場面です。特にListから別のListを作る処理では効果が分かりやすいです。
たとえば、在庫ありの商品だけを取り出す、ユーザー一覧からメールアドレスだけを取り出す、注文一覧の合計金額を求める、条件に合う最初の要素を探す、といった処理です。これらは、Stream APIのfilter、map、reduce、findFirstなどで表現しやすいです。
逆に、各要素を処理しながら外部の変数を何度も更新する、複数のAPIを呼び出して状態を変える、途中で細かくログや例外処理を挟む、といった処理は慎重に考えましょう。Stream APIを使う目的は、コードを短くすることではなく、意図を読みやすくすることです。
2. filter / map / collectの基本
2-1. filterで条件に合う要素だけ残す
filterは、条件に合う要素だけをStreamに残すためのメソッドです。リストから必要な要素だけを取り出したいときに使います。
filterには、各要素に対してtrueまたはfalseを返す条件を書きます。条件がtrueの要素は残り、falseの要素は除外されます。for文で書くとif文で条件判定して結果リストへ追加する処理に近いです。
List<User> activeUsers = users.stream()
.filter(user -> user.isActive())
.toList();
このコードでは、有効なユーザーだけをListにしています。注意点は、filterの中に状態変更や外部API呼び出しのような副作用を入れないことです。条件判定に集中させると、Streamの流れが読みやすくなります。
2-2. mapで別の形に変換する
mapは、Streamの各要素を別の形に変換するためのメソッドです。エンティティから名前だけを取り出す、DTOへ変換する、数値を別単位に変える、といった場面で使います。
mapは、入力された1つの要素から、変換後の1つの値を返します。たとえば、UserからStringの名前へ変換したり、OrderからOrderResponseへ変換したりできます。元のリストの要素を加工して、別のリストを作る処理と相性がよいです。
List<String> userNames = users.stream()
.map(User::getName)
.toList();
このコードでは、ユーザー一覧から名前だけを取り出しています。注意点は、mapの中で元のオブジェクトを書き換える使い方は避けることです。変換処理として読む人が期待するため、副作用を入れると意図が分かりにくくなります。
2-3. collectで結果をListなどにまとめる
collectは、Streamで処理した結果をList、Set、Mapなどにまとめるための終端操作です。Javaのバージョンや用途によって、toList()やCollectors.toList()を使います。
Streamのfilterやmapは、中間操作と呼ばれます。中間操作だけでは最終結果は取り出せません。最後にcollectやtoList、count、findFirstなどの終端操作を呼ぶことで、結果が作られます。
List<String> emails = users.stream()
.filter(User::isActive)
.map(User::getEmail)
.collect(Collectors.toList());
このコードでは、有効なユーザーのメールアドレスをListにまとめています。注意点は、Java 16以降ではtoList()もよく使われることです。ただし、戻り値のListの変更可否などが異なる場合があるため、プロジェクトのJavaバージョンと方針に合わせましょう。
3. for文とStreamを比較する
3-1. for文で書いた場合
for文で書く場合は、処理の手順を上から順に明示できます。途中の変数や条件が見えやすいため、初心者にも追いやすい書き方です。
たとえば、商品一覧から在庫があり、価格が1000円以上の商品名だけを取り出す処理を考えます。for文では、結果用のListを作り、1件ずつ商品を取り出し、条件に合う場合だけ名前を追加します。手順がそのままコードになるため、複雑な分岐がある場合にも対応しやすいです。
List<String> productNames = new ArrayList<>();
for (Product product : products) {
if (product.isInStock() && product.getPrice() >= 1000) {
productNames.add(product.getName());
}
}
このコードでは、条件判定と追加処理をfor文で明示しています。注意点は、処理が増えると「何をしたいのか」よりも、「どのようにループしているのか」が目立ちやすいことです。単純な絞り込みと変換なら、Streamのほうが目的を表しやすい場合があります。
3-2. Streamで書いた場合
Streamで書く場合は、絞り込み、変換、結果化の流れをメソッドチェーンで表現できます。処理の目的が単純な場合、for文より短く読みやすくなることがあります。
先ほどの処理をStreamで書くと、filterで在庫と価格条件を満たす商品だけを残し、mapで商品名に変換し、toListで結果をまとめます。コードの構造が「条件に合うものを選ぶ」「名前にする」「リストにする」という流れに近くなります。
List<String> productNames = products.stream()
.filter(product -> product.isInStock())
.filter(product -> product.getPrice() >= 1000)
.map(Product::getName)
.toList();
このコードでは、for文の条件分岐と追加処理をStreamの流れに置き換えています。注意点は、条件が複雑になりすぎた場合です。filterの中に長い条件式を詰め込むより、メソッドに切り出すか、for文に戻したほうが読みやすい場合もあります。
3-3. どちらが読みやすいかを判断する基準
for文とStreamのどちらが読みやすいかは、処理が「絞り込み・変換・集計」として自然に読めるかで判断するとよいです。Stream APIを使うこと自体を目的にしないことが大切です。
Streamは、入力コレクションから出力コレクションを作るような処理に向いています。一方、途中で複数の変数を更新する、処理順序に強く依存する、複雑な例外処理を行う、といった場合はfor文のほうが自然です。読みやすさは、コードの短さではなく、処理意図がすぐ分かるかで判断します。
判断に迷ったら、「このStreamを日本語で説明したときに、filter、map、collectの流れで素直に説明できるか」を確認しましょう。説明が難しい場合は、無理にStreamにせず、for文で分かりやすく書く選択も正解です。
4. Streamでよく使う処理
4-1. 条件に合う要素を探す
Streamでは、条件に合う要素を探す処理をfindFirstやanyMatchで表現できます。検索処理を読みやすく書きたいときに便利です。
findFirstは条件に合う最初の要素を返し、結果はOptionalになります。見つからない可能性があるため、Optionalで表現されます。anyMatchは条件に合う要素が1つでもあるかをbooleanで返します。目的が「要素を取り出したい」のか「存在確認だけしたい」のかで使い分けます。
Optional<User> user = users.stream()
.filter(value -> value.getEmail().equals("taro@example.com"))
.findFirst();
boolean hasAdmin = users.stream()
.anyMatch(User::isAdmin);
このコードでは、メールアドレスに一致するユーザーを探し、管理者が存在するかも確認しています。注意点は、findFirstの結果をすぐにget()で取り出さないことです。見つからない場合をorElseやorElseThrowで明確に扱いましょう。
4-2. 合計や件数を求める
Streamは、条件に合う件数や数値の合計を求める処理にも使えます。集計の意図を短く表現しやすいのが特徴です。
件数を求める場合はcount、数値の合計を求める場合はmapToInt、mapToLong、mapToDoubleなどを使います。注文数、在庫数、合計金額のような処理では、for文で合計用変数を更新するより、Streamで集計の目的を表せることがあります。
long activeUserCount = users.stream()
.filter(User::isActive)
.count();
int totalPrice = orders.stream()
.mapToInt(Order::getPrice)
.sum();
このコードでは、有効ユーザー数と注文金額の合計を求めています。注意点は、金額計算で小数や税率が関わる場合、doubleやintだけで済ませてよいか確認することです。業務上正確な金額計算が必要なら、BigDecimalの利用も検討します。
4-3. DTOや表示用データに変換する
Streamは、エンティティやドメインオブジェクトをDTOや表示用データへ変換する処理にも向いています。APIレスポンスを作る場面でよく使われます。
Webアプリでは、DBから取得したUserやOrderを、そのまま画面やAPIへ返すのではなく、必要な項目だけを持つDTOへ変換することがあります。このような「Listの中身を別の型に変える」処理は、mapと相性がよいです。
List<UserResponse> responses = users.stream()
.map(user -> new UserResponse(
user.getId(),
user.getName(),
user.getEmail()
))
.toList();
このコードでは、UserのListをUserResponseのListへ変換しています。注意点は、mapの中に複雑な業務ロジックを詰め込みすぎないことです。変換処理が長くなる場合は、toResponse(user)のようなメソッドに切り出すと読みやすくなります。
5. よくある落とし穴
5-1. 何でもStreamで書こうとする
Stream APIでよくある落とし穴は、何でもStreamで書こうとして、かえって読みにくくすることです。Streamは便利ですが、すべてのループ処理に向いているわけではありません。
複雑な分岐、複数の状態変更、例外処理、途中で処理を細かく分けたい場面では、for文のほうが読みやすいことがあります。Streamのメソッドチェーンが長くなりすぎると、どこで何をしているのか分かりにくくなります。短く書けることと、読みやすいことは同じではありません。
たとえば、1つのループ内で入力チェック、ログ出力、外部API呼び出し、結果の分類、エラー件数の集計をすべて行うような処理は、Streamに押し込むよりfor文で段階的に書いたほうが安全です。Stream化する前に、処理が「絞る・変換する・集める」で説明できるか確認しましょう。
5-2. forEachで副作用の多い処理を書く
forEachで副作用の多い処理を書くと、Streamの読みやすさが失われやすくなります。特に外部のListへ追加するだけの使い方は、Streamらしい書き方ではありません。
副作用とは、処理の外側にある変数を変更する、外部システムへ書き込む、ログを出す、状態を更新する、といった動きです。forEach自体が悪いわけではありませんが、Streamの途中で外部状態を変える処理が増えると、処理の流れを追いにくくなります。Listを作りたいだけなら、mapとtoListを使うほうが自然です。
// 避けたい例
List<String> names = new ArrayList<>();
users.stream()
.filter(User::isActive)
.forEach(user -> names.add(user.getName()));
// よい例
List<String> activeNames = users.stream()
.filter(User::isActive)
.map(User::getName)
.toList();
このコードでは、悪い例はforEachで外部のListを書き換えています。良い例では、Streamの結果としてListを作っています。注意点は、メール送信やDB更新など副作用が目的の処理では、Streamよりfor文のほうが意図を明確に書ける場合があることです。
5-3. Optionalやnullとの扱いで混乱する
Streamでは、findFirstなどがOptionalを返すため、nullとの扱いを混同しないことが重要です。Stream APIを使っても、null問題が自動的に消えるわけではありません。
findFirstやmax、minは、結果が存在しない可能性があるためOptionalを返します。一方、Streamの中にnull要素が混ざっていると、map(User::getName)のような処理でNullPointerExceptionが起きることがあります。Optionalは「結果がない可能性」を表すものであり、Stream内のnullをすべて安全にするものではありません。
Optional<String> firstName = users.stream()
.filter(Objects::nonNull)
.map(User::getName)
.filter(Objects::nonNull)
.findFirst();
このコードでは、nullの要素やnullの名前を除外してから最初の名前を取得しています。注意点は、そもそもコレクションにnullを入れない設計にするほうが望ましいことです。Stream側で毎回null除外を書くより、データの入口でnullを扱う方針を決めましょう。
6. まとめ
6-1. Stream APIを使う判断基準
Stream APIは、コレクションの絞り込み、変換、集計を読みやすく書くための仕組みです。for文より常に優れているわけではなく、処理内容に合わせて選ぶことが大切です。
filterは条件に合う要素を残し、mapは別の形へ変換し、collectやtoListは結果をまとめます。これらの流れで自然に説明できる処理は、Stream APIに向いています。一方、複雑な分岐や副作用が多い処理では、for文のほうが読みやすいこともあります。
Stream APIを使うか迷ったときは、「短くなるか」ではなく、「処理の意図が読みやすくなるか」で判断しましょう。新人のうちは、for文で正しく書ける処理をStreamでどう表現できるか比較しながら覚えると理解しやすいです。
6-2. 新人向けチェックリスト
Stream APIを使うときは、処理の目的、読みやすさ、副作用、nullの扱いを確認すると失敗を減らせます。
- 処理は絞り込み、変換、集計として説明できるか
filterには条件判定だけを書けているかmapには変換処理だけを書けているか- Listを作るだけなのに
forEachで外部Listを更新していないか - メソッドチェーンが長くなりすぎていないか
- 複雑な分岐を無理にStreamへ押し込んでいないか
findFirstなどのOptionalを雑にget()していないか- Stream内にnull要素が混ざる可能性を確認しているか
- for文のほうが読みやすい場面では素直にfor文を選べているか
Stream APIは、コレクション処理の意図を読みやすくするための選択肢です。for文と対立するものではなく、処理に合わせて使い分けることで、保守しやすいJavaコードを書きやすくなります。
7. 参考リンク
- Java SE Documentation: Stream
https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Stream.html - Java SE Documentation: Collectors
https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Collectors.html - Java SE Documentation: Optional
https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Optional.html