ねらいと基本原則
ストリーム処理の保守性は「副作用を禁じる」「遅延評価を味方にする」「結合可能な集約を選ぶ」の三本柱で決まります。中間操作は純粋関数(入力だけに依存し、外部状態を変更しない)に徹し、副作用は終端でのみ行うのが鉄則です。順序や並列に依存しない設計を重視し、複数の終端が必要なら再生成や材質化の分岐で扱います。
副作用禁止の実践(終端へ集約)
中間操作は純粋関数にする
中間操作に外部リスト追加やログ出力などを入れると、短絡や並列で壊れます。純粋に「入力→出力」の変換だけに限定します。
// NG(副作用あり)
List<String> out = new ArrayList<>();
data.stream().map(s -> { out.add(s); return s.toUpperCase(); }).toList();
// OK(純粋):終端で外へ流す
List<String> up = data.stream().map(String::toUpperCase).toList();
up.forEach(out::add);
Java終端でのみ外部I/Oや更新を行う
書き込み・送信・メトリクスは forEach(or forEachOrdered)で行い、並列時は順序保証が必要な箇所だけ forEachOrdered を使います。
try (var w = java.nio.file.Files.newBufferedWriter(java.nio.file.Paths.get("out.txt"))) {
lines.stream()
.map(String::trim)
.filter(s -> !s.isEmpty())
.forEachOrdered(s -> {
try { w.write(s); w.newLine(); }
catch (java.io.IOException e) { throw new java.io.UncheckedIOException(e); }
});
}
Java遅延評価を活かす順序設計
先に絞る、短絡で止める、重い処理は後段
安い filter と limit を前段に。map・sorted のような重い処理は後段へ移動し、必要最小限の要素だけ通します。
List<String> top = users.stream()
.filter(u -> u.score() >= 80) // 上流で絞る
.sorted(java.util.Comparator.comparingInt(User::score).reversed())
.limit(100) // 短絡で止める
.map(User::name) // 後段で整形
.toList();
Java複数終端は再生成か小さな材質化
ストリームは一回限り。同じ上流を使って件数とリスト両方が欲しいときは、再生成(Supplier)か、先に小さく材質化して分岐します。
java.util.function.Supplier<java.util.stream.Stream<User>> src =
() -> users.stream().filter(User::isActive);
long n = src.get().count();
java.util.List<String> names = src.get().limit(50).map(User::name).toList();
Java並列・集約の正しさ(結合可能な演算)
reduce/collect は結合律と単位元を満たす
並列でも正しくなるよう、和・件数・min/max のような結合可能な演算を用います。平均は途中で割らず、最後に sum/count で計算します。
record S(double sum, long n) {}
S s = nums.parallelStream()
.reduce(new S(0,0),
(a, x) -> new S(a.sum() + x, a.n() + 1),
(l, r) -> new S(l.sum() + r.sum(), l.n() + r.n()));
double avg = s.n() == 0 ? Double.NaN : s.sum() / s.n();
Javaグループ化は並列適性のある Collector を選ぶ
件数・合計・最大などは groupingByConcurrent と counting/summing を組み合わせます。順序に依存せず、スレッド安全に集約できます。
java.util.Map<String, Long> byCity = users.parallelStream()
.collect(java.util.stream.Collectors.groupingByConcurrent(
User::city, java.util.stream.Collectors.counting()));
Javaエラーハンドリングと Optional 設計
失敗しうる変換は Optional で落とす
想定内の不正は Optional で自然に除外し、例外で全体を止めないようにします。合流は flatMap(Optional::stream)。
static java.util.Optional<Integer> safeInt(String s) {
try { return java.util.Optional.of(Integer.parseInt(s)); }
catch (NumberFormatException e) { return java.util.Optional.empty(); }
}
java.util.List<Integer> out = strings.stream()
.map(My::safeInt)
.flatMap(java.util.Optional::stream)
.toList();
Java境界で例外化する
ドメイン的に必須値へ昇格する境界(コントローラ、保存前)で orElseThrow を用い、途中段階では Optional のまま柔軟に流します。
int id = safeInt(rawId).orElseThrow(() -> new IllegalArgumentException("id required"));
Javaイミュータブル指向と可読性の作法
イミュータブルな中間データを保つ
中間ステップでは再代入・破壊的変更を避け、不変オブジェクトを返す設計にします。デバッグ・テストが容易になり、並列でも安全です。
record UserView(long id, String name, int score) {}
java.util.List<UserView> views = users.stream()
.map(u -> new UserView(u.id(), u.name().trim(), u.score()))
.toList();
Java意味のある関数名で段階を語彙化
Predicate/Function/Comparator を名前付きで切り出し、パイプラインの意図を明確化します。
java.util.function.Predicate<String> nonBlank = s -> !s.isBlank();
java.util.function.Function<String, String> normalize = String::trim;
java.util.List<String> cleaned = lines.stream()
.map(normalize).filter(nonBlank).toList();
Java例題で手触りを掴む
クレンジング→検証→逐次出力(副作用は終端のみ)
try (var w = java.nio.file.Files.newBufferedWriter(java.nio.file.Paths.get("clean.txt"))) {
java.nio.file.Files.lines(java.nio.file.Paths.get("raw.txt"))
.map(String::trim)
.filter(s -> !s.isEmpty())
.map(s -> s.replaceAll("\\s+", " ")) // 純粋変換
.filter(s -> s.length() >= 3) // 検証
.forEachOrdered(s -> {
try { w.write(s); w.newLine(); } // 終端で副作用
catch (java.io.IOException e) { throw new java.io.UncheckedIOException(e); }
});
}
Java並列安全な頻度集計(順序非依存)
java.util.Map<String, Long> freq =
words.parallelStream()
.collect(java.util.stream.Collectors.groupingByConcurrent(
w -> w.toLowerCase(),
java.util.stream.Collectors.counting()));
Javaテンプレート(そのまま使える骨子)
副作用ゼロの基本形
java.util.stream.Stream<T> pipeline(java.util.stream.Stream<S> in,
java.util.function.Function<S, T> map,
java.util.function.Predicate<T> filter) {
return in.map(map).filter(filter);
}
Java終端で外部へ流す
static <T> void sink(java.util.stream.Stream<T> s,
java.util.function.Consumer<T> out) {
s.forEach(out); // 副作用はここだけ
}
Java複数終端の再生成
java.util.function.Supplier<java.util.stream.Stream<T>> src =
() -> list.stream().filter(this::valid);
long count = src.get().count();
java.util.List<T> top = src.get().limit(100).toList();
Java並列安全な集計
java.util.Map<K, Integer> sums = data.parallelStream()
.collect(java.util.stream.Collectors.groupingByConcurrent(
keyFn, java.util.stream.Collectors.summingInt(valFn)));
Javaまとめ
保守性の高いストリーム設計は「中間は純粋、副作用は終端」「上流で絞る・短絡で止める」「結合可能な集約で正しさを担保」「Optional で想定内の失敗を自然に落とす」「イミュータブルと語彙化で読みやすく」の徹底から生まれます。複数終端は再生成で安全に、並列は結合可能な Collector と順序非依存の設計で。これらを守れば、コードは短く、壊れにくく、将来の変更にも強いパイプラインに育ちます。

