CodeGym /コース /JAVA 25 SELF /join と concat メソッド: ストリームの結合

join と concat メソッド: ストリームの結合

JAVA 25 SELF
レベル 32, レッスン 1
使用可能

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 本の文字列にしたいとき。

比較表

メソッド 使う場面 コード例
Stream.concat(a, b)
2 つのストリーム
Stream.concat(a, b)
Stream.of(...).flatMap()
多数のストリーム、ストリームのコレクション
Stream.of(a, b, c).flatMap(x -> x)
Collectors.joining()
要素を 1 つの文字列に連結
stream.collect(Collectors.joining(", "))

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() のようなメソッドは、出力の決定的な順序を崩すことがあります。

1
タスク
JAVA 25 SELF, レベル 32, レッスン 1
ロック未解除
壮大なフルーツサラダの収穫 🍎🍊
壮大なフルーツサラダの収穫 🍎🍊
1
タスク
JAVA 25 SELF, レベル 32, レッスン 1
ロック未解除
宇宙オブジェクトのショーケース作成 🚀
宇宙オブジェクトのショーケース作成 🚀
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION