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() による List や Set への収集です。
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. 終端操作と中間操作: 用語の整理
中間操作(filter、map、distinct、sorted、limit、skip、peek)は新しい Stream を返し、チェーンできます。
終端操作(forEach、collect、count、anyMatch、allMatch、noneMatch、reduce、findFirst、findAny)はストリームを終了させ、それ以降は結果を得られません。
チェーンの例:
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: forEach を collect の代わりに使う
初心者はときどき forEach でコレクションを「集めよう」としてしまいます。例:
List<String> list = new ArrayList<>();
names.stream().forEach(name -> list.add(name)); // 動くが、これは Stream 的ではない!
代わりに collect(Collectors.toList()) を使う方が安全でクリーンです。
エラー №3: ストリームの再利用を試みる
ストリームは一度しか使えません。終端操作(たとえば collect や forEach)の後に同じ Stream を使い続けようとすると、IllegalStateException が発生します。
エラー №4: 「副作用なし」の原則に反する
中間操作は「純粋」(外部の変数を変更しない)であるべきです。map や filter の中でストリームの外側を変更するのは避けましょう。
エラー №5: Set と Map の順序を考慮していない
要素の順序が重要な場合は、適切なコレクション(例: LinkedHashSet、TreeMap)を使い、必要に応じて対応するコレクタを指定しましょう。
GO TO FULL VERSION