Stream APIとは、リストなどのデータに対する「絞り込む」「変換する」「集計する」といった処理を、メソッドをつなげて書ける仕組みです。Java 8で導入され、for文とif文で書いていた処理を、何をしたいのかが読み取りやすい形で書けます。

// for文
List<String> ids = new ArrayList<>();
for (Order o : orders) {
    if (o.paid()) {
        ids.add(o.id());
    }
}

// Stream API
List<String> ids = orders.stream()
        .filter(Order::paid)
        .map(Order::id)
        .toList();

この記事では、実務でよく使う15の書き方を、for文との対応と実際の実行結果つきで紹介します(Java 21で確認)。後半では、よくある失敗と対策もまとめています。

Stream APIの早見表

やりたいこと メソッド 種類
条件で絞り込む filter 中間操作
別の値に変換する map 中間操作
並べ替える sorted 中間操作
重複を除く distinct 中間操作
先頭から件数を絞る limit 中間操作
入れ子のリストを平らにする flatMap 中間操作
リストにする toList()(Java 16以降) 終端操作
件数を数える count 終端操作
合計・平均 mapToInt(...).sum()、average() 終端操作
条件に合うものがあるか anyMatch、allMatch、noneMatch 終端操作
最初の1件 findFirst 終端操作
グループごとに集計する collect(Collectors.groupingBy(...)) 終端操作
Mapにする collect(Collectors.toMap(...)) 終端操作
文字列をつなげる collect(Collectors.joining(", ")) 終端操作

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Stream APIの使い方|for文からの書き換え例と注意点」

ストリームの基本の形

ストリームの処理は、作る → 中間操作(何度でもつなげられる)→ 終端操作(1回だけ)の3段階です。

orders.stream()            // 1. 作る
      .filter(Order::paid) // 2. 中間操作
      .map(Order::id)      // 2. 中間操作
      .toList();           // 3. 終端操作

中間操作は、終端操作が呼ばれるまで実行されません(遅延実行)。この点は、後半の「注意点」で実際の動きを確認します。

サンプルのデータ

以降の例では、次の注文のデータを使います。

record Order(String id, String customer, String category, int price, boolean paid) {}

List<Order> orders = List.of(
    new Order("A1", "佐藤", "本",   1200,  true),
    new Order("A2", "鈴木", "家電", 30000, false),
    new Order("A3", "佐藤", "家電", 15000, true),
    new Order("A4", "田中", "本",   800,   true),
    new Order("A5", "鈴木", "食品", 2500,  true));

for文からの書き換え例15選

1. 絞り込んで変換し、リストにする(filter・map・toList)

List<String> paidIds = orders.stream()
        .filter(Order::paid)
        .map(Order::id)
        .toList();
// [A1, A3, A4, A5]

Order::paid は、o -> o.paid() の短縮形(メソッド参照)です。

2. 合計する(mapToInt・sum)

int total = orders.stream()
        .filter(Order::paid)
        .mapToInt(Order::price)
        .sum();
// 19500

数値の合計には、map ではなく mapToInt を使います。int のまま計算でき、sum() や average() が使えます。

3. 件数を数える(count)

long count = orders.stream()
        .filter(o -> o.price() >= 10000)
        .count();
// 2

4. 並べ替えて上位を取る(sorted・limit)

List<String> top2 = orders.stream()
        .sorted(Comparator.comparingInt(Order::price).reversed())
        .limit(2)
        .map(Order::id)
        .toList();
// [A2, A3]

Comparator.comparingInt(...).reversed() で、金額の高い順に並べます。

5. グループごとに件数を数える(groupingBy・counting)

Map<String, Long> countByCategory = orders.stream()
        .collect(Collectors.groupingBy(Order::category, Collectors.counting()));
// {家電=2, 本=2, 食品=1}(HashMapのため、表示の順番は保証されない)

for文で書くと、Mapに値があるかを確認して加算する処理が必要ですが、Stream APIでは1文で書けます。

6. グループごとに合計する(groupingBy・summingInt)

Map<String, Integer> totalByCustomer = orders.stream()
        .collect(Collectors.groupingBy(
                Order::customer,
                TreeMap::new,                       // キーの順に並べたいとき
                Collectors.summingInt(Order::price)));
// {佐藤=16200, 田中=800, 鈴木=32500}

groupingBy の2番目の引数でMapの種類を指定できます。HashMap(指定しない場合)は順番が保証されないため、表示の順番を揃えたいときは TreeMap::new を指定します。

7. IDから引けるMapにする(toMap)

Map<String, Integer> priceById = orders.stream()
        .collect(Collectors.toMap(Order::id, Order::price));
priceById.get("A3");
// 15000

キーが重複する場合の注意点は、後半で説明します。

8. 最初の1件を探す(findFirst・Optional)

String id = orders.stream()
        .filter(o -> o.price() > 100000)
        .map(Order::id)
        .findFirst()
        .orElse("なし");
// なし

findFirst() は、見つからない可能性があるため Optional を返します。orElse で見つからなかったときの値を指定します。get() をいきなり呼ぶと、見つからなかったときに例外になるので注意します。

9. 条件に合うものがあるか(anyMatch)

boolean hasUnpaid = orders.stream().anyMatch(o -> !o.paid());
// true
メソッド 意味
anyMatch 1つでも条件に合えば true
allMatch すべてが条件に合えば true
noneMatch 1つも条件に合わなければ true

10. 重複を除く(distinct)

List<String> customers = orders.stream()
        .map(Order::customer)
        .distinct()
        .toList();
// [佐藤, 鈴木, 田中]

11. 文字列をつなげる(joining)

String text = orders.stream()
        .map(Order::id)
        .collect(Collectors.joining(", ", "[", "]"));
// [A1, A2, A3, A4, A5]

区切り文字・先頭・末尾を指定できます。

12. 条件で2つに分ける(partitioningBy)

Map<Boolean, List<String>> byPaid = orders.stream()
        .collect(Collectors.partitioningBy(
                Order::paid,
                Collectors.mapping(Order::id, Collectors.toList())));
// {false=[A2], true=[A1, A3, A4, A5]}

13. 入れ子のリストを平らにする(flatMap)

List<List<String>> skills = List.of(List.of("java", "sql"), List.of("java", "aws"));
List<String> all = skills.stream()
        .flatMap(List::stream)
        .distinct()
        .toList();
// [java, sql, aws]

14. 最大のものを取る(max)

orders.stream()
      .max(Comparator.comparingInt(Order::price))
      .ifPresent(o -> System.out.println(o.id()));
// A2

15. 平均を求める(average)

double avg = orders.stream()
        .mapToInt(Order::price)
        .average()
        .orElse(0);
// 9900.0

よくある失敗と対策

失敗1:同じストリームを2回使う

Stream<Order> s = orders.stream();
s.count();
s.count();   // 例外
java.lang.IllegalStateException: stream has already been operated upon or closed

ストリームは1回しか使えません。もう一度処理したいときは、orders.stream() から作り直します。ストリームを変数に入れて使い回すのは避けます。

失敗2:toList()の結果に要素を追加する

List<String> ids = orders.stream().map(Order::id).toList();
ids.add("X");   // UnsupportedOperationException

Java 16で追加された toList() は、変更できないリストを返します。後から要素を追加・削除したい場合は、collect(Collectors.toList()) を使うか、new ArrayList<>(...) で包みます。

List<String> ids = orders.stream()
        .map(Order::id)
        .collect(Collectors.toList());   // 変更できるリスト
ids.add("X");   // OK
書き方 変更 使える版
.toList() できない Java 16以降
.collect(Collectors.toList()) できる(仕様上は保証なし、実装はArrayList) Java 8以降
.collect(Collectors.toCollection(ArrayList::new)) できる(保証あり) Java 8以降

失敗3:toMapでキーが重複する

orders.stream().collect(Collectors.toMap(Order::customer, Order::price));
java.lang.IllegalStateException: Duplicate key 佐藤 (attempted merging values 1200 and 15000)

同じ顧客の注文が2件あるため、キーが重複して例外になりました。重複する可能性があるときは、3番目の引数で「重複したときの合わせ方」を指定します。

Map<String, Integer> totalByCustomer = orders.stream()
        .collect(Collectors.toMap(Order::customer, Order::price, Integer::sum, TreeMap::new));
// {佐藤=16200, 田中=800, 鈴木=32500}

失敗4:中間操作だけ書いて、何も起きない

Stream<Order> s = orders.stream()
        .filter(o -> { System.out.println("filter " + o.id()); return o.paid(); });
System.out.println("終端操作の前");
s.findFirst();

実行結果は次のとおりです。

終端操作の前
filter A1

中間操作は、終端操作を呼ぶまで実行されません。また、findFirst() は1件見つかった時点で終わるため、A1しか filter を通っていません。必要な分だけ処理する仕組みのため、大量のデータでも無駄が少なくなります。逆に、終端操作を書き忘れると、処理が何も実行されません。

失敗5:parallelで外側のリストに追加する

List<Integer> unsafe = new ArrayList<>();
IntStream.range(0, 100000).parallel().forEach(unsafe::add);
System.out.println(unsafe.size());

10万件を追加したはずですが、実行するたびに結果が変わりました。

95259
92338
85702
100000
100000

parallel() は複数のスレッドで同時に処理するため、スレッドセーフではない ArrayList への追加で、要素が失われます。実行によっては例外が発生することもあります。ストリームの中で外側の変数を書き換える(副作用)のではなく、終端操作で結果を受け取る形にします。

List<Integer> safe = IntStream.range(0, 100000).parallel().boxed().toList();
// 100000(常に正しい)

parallel() は、データが多く、1件ごとの処理が重い場合に効果がありますが、件数が少ない場合はかえって遅くなることもあります。使う場合は、計測してから判断します。

失敗6:1つのストリームに処理を詰め込みすぎる

Stream APIは読みやすさが利点ですが、10行以上つながる処理や、ラムダ式の中に長い処理を書くと、かえって読みにくくなります。

  • 条件が複雑なら、isTarget(order) のようなメソッドに切り出し、filter(this::isTarget) と書く
  • 処理が複雑なら、途中の結果を変数に入れて段階を分ける
  • for文の方がわかりやすい処理(途中で break する、複数の変数を同時に更新するなど)は、無理にStream APIにしない

for文とStream APIの使い分け

場面 おすすめ
絞り込み・変換・集計を組み合わせる Stream API
グループごとの集計 Stream API(groupingBy)
途中で処理を打ち切る、複雑な条件で分岐する for文
例外(チェック例外)を投げる処理を含む for文の方が書きやすい
インデックス(何番目か)を使う for文、または IntStream.range

よくある質問

Q. Stream APIはfor文より遅いですか? A. 単純なループでは、for文の方がわずかに速いことがあります。ただし、多くの業務システムでは差が問題になることは少なく、読みやすさを優先して構いません。性能が重要な処理では、計測して判断します。

Q. forEachはどう使えばいいですか? A. 結果を画面に出力するなど、最後に何かをするときに使います。forEachの中で外側のリストに追加するような使い方は、失敗5のような問題の原因になるため、toList() や collect で結果を受け取ります。

Q. nullが含まれるリストはどう扱えばいいですか? A. filter(Objects::nonNull) で除外してから処理します。map の結果がnullになる可能性がある場合も同様です。

まとめ

  • Stream APIは、作る → 中間操作 → 終端操作の形でデータを処理する
  • よく使うのは filter・map・toList・sum・sorted・groupingBy・toMap
  • ストリームは1回しか使えない。使い回さずに作り直す
  • toList() は変更できないリストを返す。変更したいなら Collectors.toList() など
  • toMap はキーが重複すると例外。3番目の引数で合わせ方を指定する
  • 中間操作は終端操作まで実行されない(遅延実行)
  • parallelで外側の変数を書き換えない。結果は終端操作で受け取る
  • 無理にStream APIにせず、読みやすい方を選ぶ

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Java Stream APIの使い方|for文からの書き換え例と注意点」

株式会社RJCでは、Javaを使った開発の中で、読みやすく安全なコードの書き方を仲間と学び合っています。こうした知識を共有しながら、一緒に成長を楽しんでいます。

株式会社RJC ― SI事業・SES事業・AI駆動開発。未経験からエンジニアに育てるのが得意な会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?