Javaを鍛える|Java8モダン文法 #1

ラムダ式とStream API入門

  • Java
  • Java8
  • ラムダ式
  • Stream API
  • Javaを鍛える

📋 この記事の目次
  1. ラムダ式は「その場限りの一言メモ」だと考える
  2. Stream APIは「工場の加工ライン」だと考える
  3. 実践:TicketCraftの一覧処理を書き直す
  4. 挫折ポイントTOP3と対処法
  5. この先、何を学べばいいか
  6. まとめ
  7. 🎓 理解度テスト

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つのチェーンにfiltermapを5個も6個も連結してしまうと、逆に「結局何をしているコードなのか」が読み取りにくくなります。チェーンが長くなってきたら、途中で意味のある単位に分割し、中間結果に名前をつけることをおすすめします。今回の例でも、絞り込みと並び替えをあえてStep1・Step2に分けて見せたのはそのためです。


挫折ポイントTOP3と対処法

1. 終端操作を書き忘れて「何も実行されない」と焦る

中間操作だけを並べても、Streamは実際には何も処理していません(遅延評価と呼ばれる仕組みです)。collectforEachなどの終端操作を書いて初めて、一連の処理が実行されます。

2. ラムダ式の中で外部の変数を書き換えようとしてコンパイルエラーになる

ラムダ式の中から参照できる外部変数は、実質的に変更されない(実質的final)ことが前提です。カウンタ変数をラムダ式の外で用意して中でインクリメントする、といった書き方はできません。集計したい場合はcountCollectorsを活用しましょう。

3. 同じStreamを2回使おうとして例外になる

Streamは一度終端操作を実行すると、再利用できません(IllegalStateExceptionが発生します)。同じ元データに対して複数回処理をしたい場合は、その都度allTickets.stream()のように新しくStreamを生成し直します。

🧓 ベテランの現場コラム 「Streamでうまく動かない」と相談を受けたとき、まず疑うのはこの3つです。特に「終端操作の書き忘れ」は初心者がもっとも多くハマるポイントで、filtermapをいくら書いてもエラーにすらならないため、余計に気づきにくいというやっかいな特徴があります。


この先、何を学べばいいか

今回のラムダ式は、実はComparatorPredicateのような「関数型インターフェース」という型の変数に代入されています。次回は、この関数型インターフェースそのものを掘り下げ、条件やアクションを部品として使い回す方法を学びます。


まとめ

  • ラムダ式は「その場限りの一言メモ」で処理を渡せる書き方
  • Stream APIは「加工ライン」のイメージで、中間操作(filter/map/sorted等)と終端操作(forEach/collect/count等)を連結する
  • 終端操作を書かないと処理は実行されない(遅延評価)
  • チェーンが長くなりすぎたら、意味のある単位に分割する

🎓 理解度テスト

学んだ内容を確認しましょう。全5問。