- JAVA
- 技術 💻
Java Stream APIの使い方|for文からの書き換え例と注意点
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期連続の黒字決算。