概要
DB から取得した一覧を条件で絞り込み、画面用の DTO に詰め替える――この「絞り込んで変換する」処理は、業務コードの中でおそらく最も繰り返し書く形です。for ループと if の入れ子でも書けますが、一時リストへの add が増えるほど「結局どの条件で何が残るのか」が読み取りにくくなります。Stream の filter と map はこの流れを一本のパイプラインで宣言的に表現でき、コードレビューでの認識合わせも楽になります。この記事では社員一覧の絞り込みと DTO 変換を題材に、filter を map より先に置く理由、null が混ざったリストへの備え、Stream が一度しか消費できないという制約を、実行結果つきのコードで整理します。
使いどころ
社員一覧から特定部署のメンバーだけを抽出して画面用 DTO に変換する
受注データから未出荷の注文だけを絞り込んで出荷指示リストを作る
商品マスタから在庫ゼロの商品を除外して価格リストを出力する
コード例
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() + ")");
}
}
}Version Coverage
List.of で不変リスト初期化、toList() で collect 不要に。record とメソッド参照で記述が簡潔になる。
// 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
注意点
Stream は1回しか消費できない。再利用したい場合は Supplier<Stream> を使う。
副作用のある処理(DB更新など)を map 内で行わない。forEach を使う。
実務では Stream と for ループの使い分けに迷うことがある。可読性を優先するなら Stream が適しているが、途中でデバッグ出力を挟みたい場合や処理の流れを追いやすくしたい場合は for ループの方が扱いやすい。
null が混在するリストでは filter(Objects::nonNull) を先頭に入れると安全。
FAQ
先に filter で絞り込んでから map で変換する方が、変換処理の回数が減り効率的です。
チェック例外は Stream 内で直接 throw できないため、try-catch で包むか RuntimeException に変換します。
要素数が数万件以上で、各要素の処理が重い場合に効果があります。少量データでは逆にオーバーヘッドが生じます。