概要

DB から取得した一覧を条件で絞り込み、画面用の DTO に詰め替える――この「絞り込んで変換する」処理は、業務コードの中でおそらく最も繰り返し書く形です。for ループと if の入れ子でも書けますが、一時リストへの add が増えるほど「結局どの条件で何が残るのか」が読み取りにくくなります。Stream の filter と map はこの流れを一本のパイプラインで宣言的に表現でき、コードレビューでの認識合わせも楽になります。この記事では社員一覧の絞り込みと DTO 変換を題材に、filter を map より先に置く理由、null が混ざったリストへの備え、Stream が一度しか消費できないという制約を、実行結果つきのコードで整理します。

使いどころ

社員一覧から特定部署のメンバーだけを抽出して画面用 DTO に変換する

受注データから未出荷の注文だけを絞り込んで出荷指示リストを作る

商品マスタから在庫ゼロの商品を除外して価格リストを出力する

コード例

filter と map で一覧を変換する
import java.util.ArrayList;
import java.util.Objects;

public class StreamFilterMap {

    record Employee(String name, String dept, int age) {}
    record EmployeeDto(String name, int age) {}

    public static void main(String[] args) {
        // 外部データの取込を想定し、null 混じりの一覧を用意
        var employees = new ArrayList<Employee>();
        employees.add(new Employee("田中", "開発", 28));
        employees.add(new Employee("鈴木", "営業", 35));
        employees.add(null); // 欠損レコード
        employees.add(new Employee("佐藤", "開発", 42));
        employees.add(new Employee("高橋", null, 31)); // 部署未設定

        var devMembers = employees.stream()
            .filter(Objects::nonNull)             // 欠損レコードを最初に落とす
            .filter(e -> "開発".equals(e.dept())) // 定数を左辺に置けば dept が null でも落ちない
            .map(e -> new EmployeeDto(e.name(), e.age()))
            .toList();

        devMembers.forEach(System.out::println);
        // EmployeeDto[name=田中, age=28]
        // EmployeeDto[name=佐藤, age=42]

        // Stream は一度しか消費できない。使い回すと実行時例外になる
        var stream = employees.stream().filter(Objects::nonNull);
        System.out.println("1回目: " + stream.count()); // 1回目: 4
        try {
            stream.count();
        } catch (IllegalStateException e) {
            System.out.println("2回目: 再利用は不可(" + e.getMessage() + ")");
        }
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

List.of で不変リスト初期化、toList() で collect 不要に。record とメソッド参照で記述が簡潔になる。

Java 17
// Java 17: List.of + メソッド参照 + toList()
var names = List.of("Alice", "Bob", "Charlie");
var upper = names.stream()
    .filter(s -> s.length() > 3)
    .map(String::toUpperCase)
    .toList();

Library Comparison

Eclipse Collections大量データのコレクション処理でパフォーマンスを優先する場合。標準 API で十分な場合が多い。
Guava(FluentIterable / ImmutableList)Java 8 未満の環境で関数型スタイルのフィルタ・変換を書きたい場合や、不変コレクションを安全に扱いたいとき。Java 8 以降は Stream API が標準で同等の機能を提供するため、新規プロジェクトでの導入理由は薄い。既存コードで Guava に依存している場合の保守用途が主になる。

注意点

Stream は1回しか消費できない。再利用したい場合は Supplier<Stream> を使う。

副作用のある処理(DB更新など)を map 内で行わない。forEach を使う。

実務では Stream と for ループの使い分けに迷うことがある。可読性を優先するなら Stream が適しているが、途中でデバッグ出力を挟みたい場合や処理の流れを追いやすくしたい場合は for ループの方が扱いやすい。

null が混在するリストでは filter(Objects::nonNull) を先頭に入れると安全。

FAQ

filter と map の順番はどちらが先ですか?

先に filter で絞り込んでから map で変換する方が、変換処理の回数が減り効率的です。

Stream で例外が発生した場合はどうなりますか?

チェック例外は Stream 内で直接 throw できないため、try-catch で包むか RuntimeException に変換します。

並列 Stream はいつ使うべきですか?

要素数が数万件以上で、各要素の処理が重い場合に効果があります。少量データでは逆にオーバーヘッドが生じます。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます