ラムダ式とStream API入門
📋 この記事の目次
OOP基礎編で、TicketCraftのTicketクラスはずいぶん整理されました。しかし今度は別の要望が来ます。
「未対応の高優先度バグだけを、期限が近い順に並べて表示してほしい」
今のコードは、こう書かれています。
List<Ticket> result = new ArrayList<>();
for (Ticket ticket : allTickets) {
if (ticket instanceof BugTicket) {
BugTicket bug = (BugTicket) ticket;
if (bug.getStatus().equals("open") && bug.getSeverity().equals("high")) {
result.add(bug);
}
}
}
Collections.sort(result, new Comparator<Ticket>() {
@Override
public int compare(Ticket t1, Ticket t2) {
return t1.getDueDate().compareTo(t2.getDueDate());
}
});
「絞り込んで」「並び替える」というシンプルな要件のわりに、コードは長く、目的(何をしたいか)と手順(どうやるか)が混ざり合っています。今回はラムダ式とStream APIで、この処理を書き直します。
ラムダ式は「その場限りの一言メモ」だと考える
Comparatorを実装するために、わざわざnew Comparator<Ticket>() { ... }という大掛かりな匿名クラスを書くのは、部署(正式なクラス)を1つ新設して1回きりの作業を頼むようなものです。ラムダ式は、そこまで大掛かりにせず 「その場限りの一言メモ」 で処理を渡す書き方です。
Comparator<Ticket> byDueDate = (t1, t2) -> t1.getDueDate().compareTo(t2.getDueDate());
矢印->の左に引数、右に処理を書くだけで、匿名クラスと同じ意味になります。
Stream APIは「工場の加工ライン」だと考える
Stream APIは、コレクション(List等)のデータを、ベルトコンベアに乗せて順番に加工していくイメージで処理を書ける仕組みです。
- 中間操作:
filter(絞り込み)、map(変換)、sorted(並び替え)など。何度でも連結でき、まだ何も実行されていない「予約」の状態 - 終端操作:
forEach(処理の実行)、collect(結果をリスト等にまとめる)、count(件数取得)など。これが呼ばれて初めて、一連の処理が実際に実行される
実践:TicketCraftの一覧処理を書き直す
Step 1: filterで絞り込む
List<Ticket> highPriorityOpenBugs = allTickets.stream()
.filter(ticket -> ticket instanceof BugTicket)
.map(ticket -> (BugTicket) ticket)
.filter(bug -> bug.getStatus().equals("open") && bug.getSeverity().equals("high"))
.collect(Collectors.toList());
filterで条件に合わない要素を除外し、mapで型変換(TicketからBugTicketへ)を行い、最後にcollectでリストとしてまとめています。
Step 2: sortedで並び替える
List<Ticket> result = highPriorityOpenBugs.stream()
.sorted((t1, t2) -> t1.getDueDate().compareTo(t2.getDueDate()))
.collect(Collectors.toList());
匿名クラスで10行近くあったComparatorが、sortedの中の1行のラムダ式に収まりました。
Step 3: 一連の処理を1つのチェーンにまとめる
List<BugTicket> result = allTickets.stream()
.filter(ticket -> ticket instanceof BugTicket)
.map(ticket -> (BugTicket) ticket)
.filter(bug -> bug.getStatus().equals("open") && bug.getSeverity().equals("high"))
.sorted((t1, t2) -> t1.getDueDate().compareTo(t2.getDueDate()))
.collect(Collectors.toList());
「絞り込んで」「並び替えて」「まとめる」という目的が、コードの見た目からそのまま読み取れるようになりました。
Step 4: 集計にも使える
long openTicketCount = allTickets.stream()
.filter(ticket -> ticket.getStatus().equals("open"))
.count();
boolean allAssigned = allTickets.stream()
.allMatch(ticket -> ticket.getAssignee() != null);
「未対応件数を数える」「全チケットに担当者が割り当たっているか確認する」といった集計も、for文とカウンタ変数を自分で用意することなく書けます。
よく使う中間操作・終端操作
| 種類 | メソッド | 概要 |
|---|---|---|
| 中間操作 | filter |
条件に合う要素だけを残す |
| 中間操作 | map |
各要素を別の値に変換する |
| 中間操作 | sorted |
要素を並び替える |
| 中間操作 | distinct |
重複を除去する |
| 終端操作 | forEach |
各要素に対して処理を実行する |
| 終端操作 | collect |
結果をリスト等にまとめる |
| 終端操作 | count |
要素数を取得する |
| 終端操作 | allMatch |
全要素が条件を満たすか判定する |
🧓 ベテランの現場コラム Stream APIは強力ですが、1つのチェーンに
filterやmapを5個も6個も連結してしまうと、逆に「結局何をしているコードなのか」が読み取りにくくなります。チェーンが長くなってきたら、途中で意味のある単位に分割し、中間結果に名前をつけることをおすすめします。今回の例でも、絞り込みと並び替えをあえてStep1・Step2に分けて見せたのはそのためです。
挫折ポイントTOP3と対処法
1. 終端操作を書き忘れて「何も実行されない」と焦る
中間操作だけを並べても、Streamは実際には何も処理していません(遅延評価と呼ばれる仕組みです)。collectやforEachなどの終端操作を書いて初めて、一連の処理が実行されます。
2. ラムダ式の中で外部の変数を書き換えようとしてコンパイルエラーになる
ラムダ式の中から参照できる外部変数は、実質的に変更されない(実質的final)ことが前提です。カウンタ変数をラムダ式の外で用意して中でインクリメントする、といった書き方はできません。集計したい場合はcountやCollectorsを活用しましょう。
3. 同じStreamを2回使おうとして例外になる
Streamは一度終端操作を実行すると、再利用できません(IllegalStateExceptionが発生します)。同じ元データに対して複数回処理をしたい場合は、その都度allTickets.stream()のように新しくStreamを生成し直します。
🧓 ベテランの現場コラム 「Streamでうまく動かない」と相談を受けたとき、まず疑うのはこの3つです。特に「終端操作の書き忘れ」は初心者がもっとも多くハマるポイントで、
filterやmapをいくら書いてもエラーにすらならないため、余計に気づきにくいというやっかいな特徴があります。
この先、何を学べばいいか
今回のラムダ式は、実はComparatorやPredicateのような「関数型インターフェース」という型の変数に代入されています。次回は、この関数型インターフェースそのものを掘り下げ、条件やアクションを部品として使い回す方法を学びます。
まとめ
- ラムダ式は「その場限りの一言メモ」で処理を渡せる書き方
- Stream APIは「加工ライン」のイメージで、中間操作(filter/map/sorted等)と終端操作(forEach/collect/count等)を連結する
- 終端操作を書かないと処理は実行されない(遅延評価)
- チェーンが長くなりすぎたら、意味のある単位に分割する
🎓 理解度テスト
学んだ内容を確認しましょう。全5問。