1. Stream.concat: 2 つのストリームを結合する
プログラミングではよく、2 本(またはそれ以上)のデータストリームを 1 つにまとめたい場面があります。たとえば、別グループの学生リストが 2 つあり、全員を一度に処理したい場合です。昔のやり方(Stream API 以前)なら addAll で 2 つのリストを結合していました。しかしストリーム(Stream<T>)で扱っているなら、遅延的かつ表現力豊かに行いたいところです。
構文と動作原理
最も基本的な 2 本のストリームの結合方法は、静的メソッド Stream.concat を使うことです。
Stream<T> Stream.concat(Stream<? extends T> a, Stream<? extends T> b)
何が起きるか:
- まず最初のストリーム、続いて 2 つ目のストリームが取り出されます。
- 出力は新しいストリームで、最初に第 1 ストリームのすべての要素、次に第 2 ストリームのすべての要素を返します。
- 結合は遅延的です。終端操作(例: forEach や collect)で最終的なストリームを走査するまで、何も起きません。
例: 2 つの名前リストを結合する
import java.util.List;
import java.util.stream.Stream;
List<String> groupA = List.of("アーニャ", "ボリス", "ヴィカ");
List<String> groupB = List.of("グリーシャ", "ダーシャ");
Stream<String> allStudents = Stream.concat(groupA.stream(), groupB.stream());
allStudents.forEach(System.out::println);
結果:
アーニャ
ボリス
ヴィカ
グリーシャ
ダーシャ
重要: 結合後の最終ストリームは一回限りです。他の Stream と同様、再利用はできません。
Stream.concat の特徴
- 一度に 2 本のみ。 3 本以上なら concat を連鎖させるか、別の方法(下記参照)を使いましょう.
- 順序は保持。 まず第 1 ストリームの要素、その後に第 2 の要素。
- 遅延的な結合。 第 1 ストリームが無限なら、第 2 には決して到達しません。
- 典型的なバグ: ストリームを自分自身と結合すること(Stream.concat(stream, stream))は同じストリームの再利用になるため禁止です。
2. 2 本以上のストリームの結合: flatMap と Stream.of
ストリームが 2 本より多い場合は、Stream.of + flatMap の組み合わせが便利です。
Stream.of + flatMap を使う
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;
List<String> groupA = List.of("アーニャ", "ボリス");
List<String> groupB = List.of("ヴィカ");
List<String> groupC = List.of("グリーシャ", "ダーシャ");
Stream<String> allStudents = Stream.of(groupA, groupB, groupC)
.flatMap(Collection::stream);
allStudents.forEach(System.out::println);
ここでの処理:
- Stream.of(groupA, groupB, groupC) は 3 つのコレクションからストリームを作成します。
- flatMap(Collection::stream) は各コレクションを 1 本の文字列ストリームへ展開します。
- 結果は、すべての学生を含む 1 本の大きなストリームです。
利点: いくつでも結合できます。
別案: ストリームのリストを結合する
import java.util.List;
import java.util.stream.Stream;
List<Stream<String>> streams = List.of(
Stream.of("a", "b"),
Stream.of("c"),
Stream.of("d", "e")
);
Stream<String> merged = streams.stream()
.flatMap(s -> s);
merged.forEach(System.out::println);
結果:
a
b
c
d
e
3. Collectors.joining: 文字列を区切り付きで連結する
ストリームを単に結合するのではなく、すべての要素を 1 本の文字列に連結したいことがあります。たとえば名前をカンマ区切りで出力したい場合です。そんなときは収集器 Collectors.joining を使います。
構文
String result = stream.collect(Collectors.joining(", "));
- 引数なし: 区切りなしで文字列を連結します。
- 区切りあり: 要素間に指定した文字列を挿入します。
- 接頭辞と接尾辞: Collectors.joining(delimiter, prefix, suffix)
例: 学生リストを 1 行の文字列に
import java.util.List;
import java.util.stream.Collectors;
List<String> students = List.of("アーニャ", "ボリス", "ヴィカ");
String line = students.stream()
.collect(Collectors.joining(", "));
System.out.println("学生一覧: " + line);
結果:
学生一覧: アーニャ, ボリス, ヴィカ
接頭辞と接尾辞の例
String line = students.stream()
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(line);
結果:
[アーニャ, ボリス, ヴィカ]
用途:
- CSV 文字列の生成。
- リストやレポートの見やすい出力。
- データを 1 行の文字列で渡す(例: URL やログ)。
4. concat と flatMap の比較: 使い分けは?
- Stream.concat — ストリームがちょうど 2 本で、順序を保ちたいとき。
- Stream.of + flatMap — ストリームが多数ある、またはコレクションに格納されているとき。
- Collectors.joining — 要素が String で、1 本の文字列にしたいとき。
比較表
| メソッド | 使う場面 | コード例 |
|---|---|---|
|
2 つのストリーム | |
|
多数のストリーム、ストリームのコレクション | |
|
要素を 1 つの文字列に連結 | |
5. 学習用アプリでの実例
例 1. 2 学部の学生を結合する
import java.util.List;
import java.util.stream.Stream;
List<String> itStudents = List.of("アーニャ", "ボリス");
List<String> mathStudents = List.of("ヴィカ", "グリーシャ");
Stream<String> all = Stream.concat(itStudents.stream(), mathStudents.stream());
all.forEach(System.out::println);
例 2. すべてのグループの学生を結合し、1 行に出力する
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
List<List<String>> allGroups = List.of(
List.of("アーニャ", "ボリス"),
List.of("ヴィカ"),
List.of("グリーシャ", "ダーシャ")
);
String allNames = allGroups.stream()
.flatMap(Collection::stream)
.collect(Collectors.joining("; "));
System.out.println("全学生: " + allNames);
結果:
全学生: アーニャ; ボリス; ヴィカ; グリーシャ; ダーシャ
例 3. 数値ストリームを結合する
import java.util.stream.Stream;
Stream<Integer> s1 = Stream.of(1, 2, 3);
Stream<Integer> s2 = Stream.of(4, 5);
Stream<Integer> merged = Stream.concat(s1, s2);
merged.forEach(System.out::print); // 12345
6. 重要な注意点と特徴
concat は 2 本のストリームにのみ対応
3 本の場合は入れ子の記述になり、動作はしますが見た目が騒がしくなります。Stream.of + flatMap の使用をおすすめします。
Stream<Integer> s1 = Stream.of(1);
Stream<Integer> s2 = Stream.of(2);
Stream<Integer> s3 = Stream.of(3);
Stream<Integer> merged = Stream.concat(Stream.concat(s1, s2), s3);
ストリームは一度きり
終端操作の後はストリームを使用できません。再利用すると IllegalStateException が発生します。
concat のストリームは遅延評価
第 1 ストリームが無限(例えば Stream.generate(...) で作成)なら、第 2 には決して到達しません。
要素の順序
順序は常に保持されます。まず第 1 ストリーム、次に第 2 ストリームです。
Collectors.joining は Stream<String> にのみ対応
非文字列の場合は、まず文字列に変換してください。例えば map(Object::toString) を使用します。
import java.util.stream.Collectors;
import java.util.stream.Stream;
Stream<Integer> numbers = Stream.of(1, 2, 3);
String line = numbers
.map(Object::toString)
.collect(Collectors.joining(", "));
System.out.println(line); // 1, 2, 3
「手作業」のやり方との比較
Stream API 以前:
import java.util.ArrayList;
import java.util.List;
List<String> merged = new ArrayList<>(listA);
merged.addAll(listB);
Stream API の場合:
import java.util.stream.Collectors;
List<String> merged = Stream.concat(listA.stream(), listB.stream())
.collect(Collectors.toList());
ストリームの利点: 結合の前後に中間操作(filter、map など)を差し込めるほか、あらゆるデータソースと組み合わせて使えます。
7. ストリーム結合時のよくあるミス
エラー1: ストリームの再利用。 ストリームは 1 回しか使えません。自分自身との結合は不可です。
import java.util.stream.Stream;
Stream<String> s = Stream.of("a", "b");
Stream<String> merged = Stream.concat(s, s); // エラー! s が再利用されてしまう
エラー2: 無限ストリームと有限ストリームを結合。 第 1 ストリームが無限なら、第 2 は決して始まりません。
import java.util.stream.Stream;
Stream<Integer> infinite = Stream.generate(() -> 1);
Stream<Integer> finite = Stream.of(2, 3);
Stream<Integer> merged = Stream.concat(infinite, finite);
// merged.limit(10).forEach(System.out::println); // finite は結果に入らない
エラー3: joining を非文字列に使う。 収集器 Collectors.joining は文字列ストリームを受け取ります。数値には事前に文字列化が必要です。
import java.util.List;
import java.util.stream.Collectors;
List<Integer> numbers = List.of(1, 2, 3);
// numbers.stream().collect(Collectors.joining(", ")); // コンパイルエラー!
String joined = numbers.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
エラー4: 順序の破壊。 順序が重要な場合は concat または順序を保つ flatMap を使いましょう。unordered() のようなメソッドは、出力の決定的な順序を崩すことがあります。
GO TO FULL VERSION