CodeGym /コース /JAVA 25 SELF /Stream を使ったコレクション変換

Stream を使ったコレクション変換

JAVA 25 SELF
レベル 30 , レッスン 4
使用可能

1. List → Set の変換とその逆

そもそもなぜコレクションの変換が必要なのでしょうか。実務では次のような場面がよくあります。

  • リストから重複を取り除いて一意な要素だけを得たい(例: e-mail のリスト → 一意なアドレス集合)。
  • マップ(Map)を構築したい。たとえば名前のリストから「名前 → 名前の長さ」のマップを作る。
  • 要素を1つの文字列に結合したい(きれいに出力するためなど)。

以前はこれを実現するのに、ループや条件分岐、一時コレクションを使って多くのコードを書く必要がありました。Stream API を使えば、ずっと簡単でしかもスマートにできます。

例: リストから一意な名前の集合を得る

たとえば名前のリストがあるとします(誰かが同じ名前を二度入力してしまうこともあります)。

List<String> names = List.of("Anna", "Sergey", "Anna", "Maria", "Ivan", "Sergey");

目的は各名前が一度だけ現れるコレクション、つまり集合(Set)にすることです。Stream API を使えば、次の1行でできます。

Set<String> uniqueNames = names.stream()
    .collect(Collectors.toSet());
System.out.println(uniqueNames);

出力:

[Maria, Ivan, Anna, Sergey]

Set の順序は保証されません。環境によって順序が異なっても驚かないでください。

逆はどうする: Set → List?

逆に集合をリストに変換したいこともあります(たとえばソートしたい、インデックスでアクセスしたいなど)。

List<String> namesList = uniqueNames.stream()
    .collect(Collectors.toList());
System.out.println(namesList);

2. Map への変換: Collectors.toMap()

例: 名前のリストから「名前 → 名前の長さ」の Map を作る

たまにはプログラマだけでなく地図職人にもなってみましょう。マップを作成します。

List<String> names = List.of("Anna", "Sergey", "Maria", "Ivan");

Map<String, Integer> nameToLength = names.stream()
    .collect(Collectors.toMap(
        name -> name,         // キー: 名前そのもの
        name -> name.length() // 値: 名前の長さ
    ));

System.out.println(nameToLength);

出力:

{Maria=5, Ivan=4, Anna=4, Sergey=6}

重要な点: キーの重複

元のリストに同じ名前が含まれていると、Map に集約する際に IllegalStateException: Duplicate key が発生します。Java は同じキーに2つの値を入れようとするのを嫌います。

重複をどう処理する?
キーが衝突したときの方針を指定できます。たとえば最初の値を残すか、最後の値を残すかなど。

List<String> names = List.of("Anna", "Sergey", "Anna", "Maria", "Ivan", "Sergey");

Map<String, Integer> nameToLength = names.stream()
    .collect(Collectors.toMap(
        name -> name,
        name -> name.length(),
        (oldValue, newValue) -> oldValue // 最初の値を残す
    ));

System.out.println(nameToLength);

これでプログラムは落ちず、Map には各名前の最初の出現だけが入ります。

例: オブジェクトの Map

もう少し複雑にしてみましょう。ユーザーのリストがあり、「名前 → ユーザー」の Map を作ります。

class User {
    String name;
    int age;
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String toString() {
        return name + " (" + age + ")";
    }
}

// ユーザー一覧の例
List<User> users = List.of(
    new User("Anna", 25),
    new User("Sergey", 30),
    new User("Maria", 22)
);

Map<String, User> nameToUser = users.stream()
    .collect(Collectors.toMap(
        user -> user.name,
        user -> user
    ));

System.out.println(nameToUser);

出力:

{Maria=Maria (22), Anna=Anna (25), Sergey=Sergey (30)}

3. 文字列への連結: Collectors.joining()

コレクションを集めるだけでなく、ユーザー表示やログのために見栄えのよい文字列にしたいこともあります。例えば、すべての名前をカンマ区切りで連結します。

List<String> names = List.of("Anna", "Sergey", "Maria", "Ivan");

String result = names.stream()
    .collect(Collectors.joining(", "));

System.out.println(result);

出力:

Anna, Sergey, Maria, Ivan

プレフィックスとサフィックスを追加する

String result = names.stream()
    .collect(Collectors.joining(", ", "リスト: [", "]"));

System.out.println(result);

出力:

リスト: [Anna, Sergey, Maria, Ivan]

4. 終端操作: forEach, collect, count, anyMatch, allMatch, noneMatch

forEach メソッド

forEach はおなじみですね。ストリームの各要素に対して処理を実行する終端操作です。

names.stream().forEach(name -> System.out.println("こんにちは、" + name + "!"));

collect メソッド

要素をコレクション、文字列、その他の構造に集約します。もっともよく使うのは、Collectors.toList()Collectors.toSet() による ListSet への収集です。

count メソッド

ストリーム内の要素数を数えます.

long count = names.stream()
    .filter(name -> name.length() > 4)
    .count();
System.out.println("4文字より長い名前の数: " + count);

anyMatch, allMatch, noneMatch メソッド

条件が少なくとも1つの要素に当てはまるか(anyMatch)、すべてに当てはまるか(allMatch)、どれにも当てはまらないか(noneMatch)を判定します。

boolean hasShortName = names.stream()
    .anyMatch(name -> name.length() < 4);
System.out.println("短い名前はある? " + hasShortName);

boolean allLong = names.stream()
    .allMatch(name -> name.length() > 3);
System.out.println("すべての名前は3文字より長い? " + allLong);

boolean noneIvan = names.stream()
    .noneMatch(name -> name.equals("Ivan"));
System.out.println("Ivan はいない? " + noneIvan);

出力:

短い名前はある? false
すべての名前は3文字より長い? true
Ivan はいない? false

5. 終端操作と中間操作: 用語の整理

中間操作filtermapdistinctsortedlimitskippeek)は新しい Stream を返し、チェーンできます。

終端操作forEachcollectcountanyMatchallMatchnoneMatchreducefindFirstfindAny)はストリームを終了させ、それ以降は結果を得られません。

チェーンの例:

List<String> result = users.stream()
    .filter(user -> user.age > 20)
    .map(user -> user.name.toUpperCase())
    .distinct()
    .sorted()
    .collect(Collectors.toList());

System.out.println(result);

出力:

[ANNA, IVAN, MARIA, SERGEY]

6. Stream によるコレクション変換でありがちなエラー

エラー №1: toMap で重複キーを処理していない
元のコレクションに重複キーがあるのに Collectors.toMap() をマージ関数なしで使うと、例外が投げられます。こうした場合は必ずマージ関数を指定しましょう。

// 最後の値を残す
.toMap(keyMapper, valueMapper, (oldVal, newVal) -> newVal)

エラー №2: forEachcollect の代わりに使う
初心者はときどき forEach でコレクションを「集めよう」としてしまいます。例:

List<String> list = new ArrayList<>();
names.stream().forEach(name -> list.add(name)); // 動くが、これは Stream 的ではない!

代わりに collect(Collectors.toList()) を使う方が安全でクリーンです。

エラー №3: ストリームの再利用を試みる
ストリームは一度しか使えません。終端操作(たとえば collectforEach)の後に同じ Stream を使い続けようとすると、IllegalStateException が発生します。

エラー №4: 「副作用なし」の原則に反する
中間操作は「純粋」(外部の変数を変更しない)であるべきです。mapfilter の中でストリームの外側を変更するのは避けましょう。

エラー №5: SetMap の順序を考慮していない
要素の順序が重要な場合は、適切なコレクション(例: LinkedHashSetTreeMap)を使い、必要に応じて対応するコレクタを指定しましょう。

1
アンケート/クイズ
Stream API の基礎、レベル 30、レッスン 4
使用不可
Stream API の基礎
Stream API の基礎
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION