Javaのfor文とStreamの使い分けを解説する記事のアイキャッチ。若手エンジニアがJava道場風の学習空間で繰り返し処理を学ぶイラスト。

実装・エラー解決

Javaのfor文とStreamの使い分け

AIによる要約

Javaには通常のfor文、拡張for文、Streamなど複数の繰り返し処理があります。どれが正解かは場面によります。この記事では、初学者が迷いやすい使い分けを、現場の可読性とデバッグしやすさの観点で整理します。

新人SE
新人SE
Streamを使った方が新しい書き方で、全部Streamにした方がいいんですか?
ポンコツSE
ポンコツSE
そうとは限りません。現場では、短く書けることより、チームが読みやすくデバッグしやすいことが優先されます。

Javaのコレクション処理では、同じ結果を複数の書き方で実現できます。

初学者が迷いやすいのは、for文が古くて、Streamが常に良い書き方に見えることです。

この記事のポイント

  • 単純な繰り返しは拡張for文で十分なことが多い
  • 抽出・変換・集計はStreamが読みやすい場合がある
  • 途中に複雑な処理やデバッグが必要ならfor文が強い
  • チームの読みやすさを優先して選ぶ

拡張for文は現場でも普通に使う

一覧を順番に処理するだけなら、拡張for文は今でも十分に読みやすいです。

for (Order order : orders) {
    if (order.isExpired()) {
        expiredOrders.add(order);
    }
}

条件分岐やログ出力、途中の変数確認が必要な処理では、for文の方が追いやすいことがあります。

Streamが向いている処理

Streamは、一覧から条件に合うものを取り出す、別の形に変換する、集計する、といった処理で読みやすくなります。

List<String> activeUserNames = users.stream()
        .filter(User::isActive)
        .map(User::getName)
        .toList();

このように、処理の流れが「絞る、取り出す、集める」と読めるならStreamは有効です。

読みにくいStreamは避ける

問題は、Streamを使うこと自体が目的になってしまうケースです。処理が長くなり、ラムダの中に条件分岐や副作用が増えると、かえって読みにくくなります。

避けたいStreamの例

  • 1つのStreamチェーンが長すぎる
  • forEachの中で外部の変数を更新している
  • 例外処理やログ出力が混ざっている
  • デバッグ時に中間値を追いにくい

現場での選び方

使い分けの目安

  • 単純な繰り返しなら拡張for文
  • 一覧の変換・抽出ならStream
  • インデックスが必要なら通常のfor文
  • 複雑な業務ロジックが入るなら無理にStream化しない

レビューでは「新しい書き方か」より、次に読む人が意図を追えるかが見られます。

現場ではデバッグしやすさも選定基準になる

たとえば、入金済みで期限が切れた注文のうち、10,000円以上を担当者の確認対象にします。同じ注文6件を入力順に処理すると、9,999円の注文61は外れ、10,000円の注文42は残ります。

注文ID入金・期限金額判定
42入金済み・期限切れ10,000円対象
72未入金・期限切れ12,000円未入金
81入金済み・期限内12,000円期限内
61入金済み・期限切れ9,999円10,000円未満
14入金済み・期限切れ12,000円対象
95未入金・期限内5,000円未入金

対象一覧だけが必要なら、Streamでは3つの条件を上から読み、eligibleOrders(orders).toList()で結果を集められます。次は配布ファイルのメソッド抜粋です。

static Stream<Order> eligibleOrders(List<Order> orders) {
    return orders.stream()
            .filter(Order::paid)
            .filter(Order::expired)
            .filter(order -> order.amount() >= 10_000);
}

「なぜこの注文が一覧に出なかったか」も調べたいなら、for文の各分岐で、注文IDと最初に満たさなかった条件を記録できます。注文95は期限内で金額も足りませんが、この順で確認すると最初の除外理由は未入金です。次は同じファイルのメソッド抜粋です。

static Selection selectWithFor(List<Order> orders) {
    List<Order> targets = new ArrayList<>();
    List<Exclusion> exclusions = new ArrayList<>();
    for (Order order : orders) {
        if (!order.paid()) {
            exclusions.add(new Exclusion(order.id(), "未入金"));
            continue;
        }
        if (!order.expired()) {
            exclusions.add(new Exclusion(order.id(), "期限内"));
            continue;
        }
        if (order.amount() < 10_000) {
            exclusions.add(new Exclusion(order.id(), "10,000円未満"));
            continue;
        }
        targets.add(order);
    }
    return new Selection(targets, exclusions);
}

どちらも対象は注文42、14の順です。結果に後から追加する仕様がある場合は、Stream.toList()の結果は変更不可です。Streamでも追加できる一覧を返すなら、collect(Collectors.toCollection(ArrayList::new))で可変の一覧に集めます。

同じ注文6件を実行して比べる

ZIPにOrderFilterDemo.javaの全文と手順書を収録しています。展開先でJDK 17を使い、java OrderFilterDemo.javaを実行すると、対象の順序と除外理由を確認できます。

レビューではチームの読みやすさが見られる

新人がやりがちなのは、Streamを覚えたばかりで何でもStream化することです。レビューでは、チーム全員が読めるか、既存コードの書き方と大きくズレていないかも見られます。

// レビューコメント例
// この処理は条件分岐が多く、今後ログ出力も入りそうです。
// Streamで1行に寄せるより、for文で条件ごとに分けた方が保守しやすそうです。

for文とStreamの判断基準

  • 抽出・変換・集計が素直に読めるならStream
  • 途中でログやデバッグが必要ならfor文
  • 条件分岐が増えてきたら無理にStream化しない
  • 既存コードの書き方とチームの慣れも見る

まとめ

for文とStreamは優劣ではなく使い分けです。初現場では、短さよりも読みやすさとデバッグしやすさを基準に選びましょう。

-実装・エラー解決
-,