1. エラー: ストリームの再利用
Java の Stream は一度きりの道具です。collect()、forEach()、count() などの終端操作を実行した瞬間、ストリームは閉じられ、以後は再利用できません。
誤りの例
Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println); // 問題なし
stream.forEach(System.out::println); // IllegalStateException が送出される!
出力:
a
b
c
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
回避方法
複数の操作を行いたい場合は、その都度新しいストリームを作成します:
List<String> list = List.of("a", "b", "c");
list.stream().forEach(System.out::println); // OK
list.stream().count(); // OK
小ワザ: ストリームは学食の使い捨てコップのようなもの。使ったら捨てて、次の飲み物には新しいコップを使いましょう。
2. エラー: ストリーム処理中にコレクションを変更する
ストリームで走査している最中に元のコレクションを変更すると ConcurrentModificationException が発生します。フィルタや削除でよくある痛点です。
誤りの例
List<String> names = new ArrayList<>(List.of("Alice", "Bob", "Charlie"));
names.stream().forEach(name -> {
if (name.startsWith("A")) {
names.remove(name); // 例外発生!
}
});
結果:
Exception in thread "main" java.util.ConcurrentModificationException
正しいやり方
走査中にコレクションを変更しないメソッドを使うか、結果を新しいコレクションに集めましょう:
names.removeIf(name -> name.startsWith("A")); // OK。removeIf は安全
// または Stream API を使う:
List<String> filtered = names.stream()
.filter(name -> !name.startsWith("A"))
.collect(Collectors.toList());
覚えておきましょう: ストリームは純粋性を好みます — ストリームが「消費している」間は元のコレクションに触れないこと。
3. エラー: 無限ストリームで limit を付け忘れる
Stream.iterate と Stream.generate は無限ストリームを生成できます。limit でサイズを制限し忘れると、処理が永遠に終わらない(または OutOfMemoryError になる)ことがあります。
誤りの例
Stream<Integer> infinite = Stream.iterate(0, n -> n + 1);
infinite.forEach(System.out::println); // プログラムは終了しません!
正しいやり方
limit でストリームを制限しましょう:
Stream<Integer> finite = Stream.iterate(0, n -> n + 1).limit(10);
finite.forEach(System.out::println); // 0 から 9 までを出力
ヒント: 無限ストリームは最終回のない連続ドラマのようなものです。止めるのはあなたの役目です。
4. エラー: null への不適切な対応
Stream API は null を嫌います。コレクションに null があると、多くの操作で NullPointerException が投げられます。特に要素に対してメソッドを呼び出す場合に顕著です。
誤りの例
List<String> names = Arrays.asList("Alice", null, "Bob");
names.stream()
.map(String::toUpperCase) // null で例外!
.forEach(System.out::println);
結果:
Exception in thread "main" java.lang.NullPointerException
正しいやり方
null 値は先にフィルタで除外しましょう:
names.stream()
.filter(Objects::nonNull)
.map(String::toUpperCase)
.forEach(System.out::println);
Optional の扱い
Stream<Optional<T>> がある場合は、(Java 9+)の flatMap(Optional::stream) で「展開」します:
List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty(), Optional.of("B"));
optionals.stream()
.flatMap(Optional::stream)
.forEach(System.out::println); // "A" と "B" だけを出力
ヒント: コレクションにはできるだけ null を入れないようにしましょう。避けられない場合は最初にフィルタで取り除きます。
5. エラー: 要素順序の喪失
一部の操作(例えば、flatMap、concat)は、特に並列ストリームを使ったり異なるコレクションを結合する場合に、要素の順序に影響することがあります。
誤りの例
List<Integer> list1 = List.of(1, 2, 3);
List<Integer> list2 = List.of(4, 5, 6);
Stream<Integer> combined = Stream.concat(list1.stream(), list2.stream());
combined.forEach(System.out::print); // 123456 — 順序は維持される
// しかし並列ストリームを使うと:
Stream<Integer> par = Stream.concat(list1.parallelStream(), list2.parallelStream());
par.forEach(System.out::print); // 順序が期待どおりでないことがある!
正しいやり方
順序が重要なら、並列ストリームを避けるか、順序を保証するメソッド(forEachOrdered)を使いましょう:
par.forEachOrdered(System.out::print);
事実: Stream API は、あなたが自分で壊さない限り、デフォルトで順序を保持します。
6. エラー: 非効率なコレクションの結合
集合演算には Set を使うと重複を避けつつ性能も良好です。さらに、flatMap で書ける場面で入れ子ループを使うのは非効率です。
誤りの例
List<String> list1 = List.of("a", "b", "c");
List<String> list2 = List.of("b", "c", "d");
// 非効率:
List<String> union = new ArrayList<>(list1);
for (String s : list2) {
if (!union.contains(s)) { // List の contains は O(n)
union.add(s);
}
}
// Set と Stream API なら効率的:
Set<String> unionSet = Stream.concat(list1.stream(), list2.stream())
.collect(Collectors.toSet());
差集合の場合:
// 非効率:
List<String> diff = new ArrayList<>(list1);
diff.removeAll(list2); // OK。ただし stream と list.contains を組み合わせていたら遅くなる
// 効率的:
Set<String> set2 = new HashSet<>(list2);
List<String> difference = list1.stream()
.filter(s -> !set2.contains(s))
.collect(Collectors.toList());
ヒント: 一意な要素を扱うときは常に Set を使いましょう。大きなコレクションでは入れ子ループを避け、stream + flatMap を優先します。
7. エラー: 終端操作の使い回し
ストリームを二手に分けて、要素をリストに集めつつ同時に件数も数えたい、と思うことがあります。しかしストリームは「一回きり」で、二つ目の操作はエラーになります。
誤りの例
Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
long count = stream.count(); // 例外発生(IllegalStateException)
正しいやり方
ストリームを複数回使う必要がある場合は、まずコレクションに集めましょう:
List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());
long count = list.size();
ヒント: 何もかも同時にやりたい場合でも、まずコレクションに集めるのが近道です。
8. エラー: リソースの close() の閉じ忘れ
リソースに紐づくストリーム(例: Files.lines(Path))を使う場合、使い終わったら必ず閉じてリークを防ぎましょう。
誤りの例
Stream<String> lines = Files.lines(Path.of("file.txt"));
lines.forEach(System.out::println); // OK だがリソースが閉じられていない!
正しいやり方
try-with-resources を使います:
try (Stream<String> lines = Files.lines(Path.of("file.txt"))) {
lines.forEach(System.out::println);
}
ヒント: ファイルに関わるストリームは電気ケトルのようなもの。沸かしたら電源を切る(=閉じる)!
9. エラー: Optional と flatMap における null/空 の誤った扱い
List<Optional<T>> のようなコレクションを扱う際に map(Optional::get) を使うと、中身が空の場合に例外になります。
誤りの例
List<Optional<String>> optionals = List.of(Optional.of("A"), Optional.empty());
optionals.stream()
.map(Optional::get) // 例外発生(NoSuchElementException)
.forEach(System.out::println);
正しいやり方
flatMap(Optional::stream)(Java 9+)を使いましょう:
optionals.stream()
.flatMap(Optional::stream)
.forEach(System.out::println); // "A" のみ
ヒント: Optional は単なるラッパーではなく、不在を安全に扱うための道具です。
10. エラー: equals/hashCode の不適切な実装
集合演算(Set、distinct、和集合・積集合)は、equals と hashCode の正しい実装に依存します。
誤りの例
class Student {
String name;
int age;
// equals と hashCode がない!
}
Set<Student> students = new HashSet<>();
students.add(new Student("Alice", 20));
students.add(new Student("Alice", 20));
System.out.println(students.size()); // 2。同一とみなされるべきなのに!
正しいやり方
クラスを Set や Map で使ったり、distinct を行う予定があるなら、equals と hashCode を必ずオーバーライドしましょう。
11. エラー: collect 後に同じ stream を使う
終端操作(例えば collect)の後はストリームが閉じられます。同じストリームで処理を続けようとするとエラーになります。
誤りの例
Stream<String> stream = Stream.of("a", "b", "c");
List<String> list = stream.collect(Collectors.toList());
stream.filter(s -> s.startsWith("a")).forEach(System.out::println); // Exception!
正しいやり方
コレクションから新しいストリームを作成します:
list.stream().filter(s -> s.startsWith("a")).forEach(System.out::println);
12. エラー: parallelStream の誤用
多くの人は parallelStream() が常に高速化すると考えますが、実際にはコレクションが小さかったり処理が軽い場合、スレッド管理のオーバーヘッドで遅くなることがあります。
誤りの例
List<Integer> numbers = IntStream.range(0, 10).boxed().collect(Collectors.toList());
numbers.parallelStream().forEach(System.out::println); // 通常の stream() より遅くなることがある
正しいやり方
parallelStream は、本当に大きなコレクションや重い計算に対してのみ使用してください。特に、副作用がなく結合的(associative)な操作に向いています。
13. エラー: 終端オペレーターの呼び忘れ
中間操作(map、filter、flatMap など)は、終端オペレーター(collect、forEach、count など)が呼ばれるまで実行されません。
誤りの例
Stream<String> stream = Stream.of("a", "b", "c").map(String::toUpperCase);
// 何も起こりません!
正しいやり方
必ず終端オペレーターを呼び出してください:
stream.forEach(System.out::println);
GO TO FULL VERSION