Collectors.toList をざっくり一言でいうと
Collectors.toList() は、
「Stream の結果を List に集め直すための“まとめ方テンプレ”」
です。
stream().filter(...).map(...) と“流して加工したもの”を、
最後に collect(Collectors.toList()) と書くことで
「はい、この結果を List にしてください」
と命令している、というイメージで OK です。
collect が「どうやって集めるか?」を指定する場所で、Collectors.toList() は「List に集める」という“パターン”を表すオブジェクト、という関係です。
一番基本の形:filter + map + Collectors.toList
例題:3 文字以下の名前だけを List にする
まずは王道パターンからいきます。
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ToListBasic {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Carol", "Dan");
List<String> shortNames =
names.stream()
.filter(name -> name.length() <= 3) // 3文字以下に絞る
.map(String::toUpperCase) // 大文字に変換
.collect(Collectors.toList()); // List に集める
System.out.println(shortNames); // [BOB, DAN]
}
}
Java流れを日本語で追うと、
元の names から stream() を作るfilter で条件に合う要素だけ残すmap で形を変える(大文字にする)collect(Collectors.toList()) で、流れてきた要素を新しい List に詰め直す
こうなっています。
ここでのポイントは、filter や map の時点ではまだ Stream<String> であって、
「List<String> になっていない」という点です。
「結果を List として使いたい」
「メソッドの戻り値に List を返したい」
となった瞬間に、Collectors.toList() の出番になります。
collect と Collectors.toList の関係をちゃんと理解する
collect の「型」だけざっくり見る
collect は Stream の終端操作の一つで、シグネチャはざっくりこうです。
<R, A> R collect(java.util.stream.Collector<? super T, A, R> collector)
Javaやっていることを噛み砕くと、
「T のストリームを、R という形にまとめるための“やり方(Collector)”を渡すと、
その通りまとめて R を返します」
というメソッドです。
この「やり方」が Collector というインターフェースで、Collectors クラスが、その具体的な実装(テンプレ)をたくさん用意してくれています。
Collectors.toList() はその一つで、
「ストリームの要素を全部集めて、List にしてください」
というルールを持った Collector オブジェクトです。
つまり、
collect(まとめ方)collect(Collectors.toList())
という形になっている、という理解で十分です。
Collectors.toList を使う場面を感覚でつかむ
メソッドの戻り値として List が欲しいとき
よくあるのが、メソッドで「条件に合う要素だけ返したい」という場面です。
従来の for 文だとこう書きます。
List<String> filterShortNames(List<String> names) {
List<String> result = new ArrayList<>();
for (String name : names) {
if (name.length() <= 3) {
result.add(name);
}
}
return result;
}
JavaStream と Collectors.toList() を使うと、こう書けます。
import java.util.List;
import java.util.stream.Collectors;
List<String> filterShortNames(List<String> names) {
return names.stream()
.filter(name -> name.length() <= 3)
.collect(Collectors.toList());
}
Java関数として、
「入力 List のうち、条件に合う要素だけを別の List にして返す」
という意図が、一行でかなり素直に表現できています。
中間結果を List として一旦保持したいとき
Stream の途中で「一旦 List に落としてから、また別の処理をしたい」
という場面でもよく使います。
List<String> filtered =
names.stream()
.filter(name -> name.length() <= 3)
.collect(Collectors.toList());
// 別のメソッドに渡したり、for 文で回したり
useListSomewhere(filtered);
Java「Stream のままでは扱いづらい」「デバッグで中身を見たい」
「一度 List に出してから、別の API に渡したい」
こういうときに、Collectors.toList() で一回「普通の List」に戻してあげる、という使い方をします。
toList と Arrays.asList の違い・使い分け
Arrays.asList は「配列 → List」、toList は「Stream → List」
例えばこんなコードがあります。
List<String> list1 = Arrays.asList("A", "B", "C"); // 配列から
List<String> list2 = stream.collect(Collectors.toList()); // Stream から
JavaArrays.asList は「配列(または可変長引数)から List を作る」ためのメソッドです。Collectors.toList() は「Stream の結果を List にする」ための Collector です。
どちらも「List を作る」という点では似ていますが、
入口(元データ)と利用シーンが違います。
もともと配列や可変長引数しかないなら Arrays.asList
Stream で一連の処理をしたあとなら collect(Collectors.toList())
と使い分けるイメージです。
Java 16 以降の Stream#toList との違い
stream.toList() と collect(toList()) は何が違うのか
Java 16 以降、Stream に toList() メソッドが追加されました。
List<String> list = names.stream()
.filter(...)
.map(...)
.toList();
Java見た目には collect(Collectors.toList()) と同じです。
細かい話をすると、
stream.toList() が返す List は「基本的に変更不可」に近い性質Collectors.toList() が返す List は実装に依存する(多くは ArrayList で変更可能)
などの差がありますが、初心者のうちは
「単に List にしたいだけなら toList() で良い」
「collect と他の Collector(toSet, groupingBy など)と合わせて使うことを覚える」
くらいで十分です。
どのみち、collect は toList 以外の場面で必須になるので、
「toList() は collect(toList()) の略記版が増えた」程度の認識で問題ありません。
Collectors.toList を使うときに意識しておきたいこと
Stream の「終点」であるという感覚
collect(Collectors.toList()) は終端操作なので、ここを呼んだ瞬間に
Stream のパイプラインが一気に実行される
Stream は「消費済み」となり、二度と使えない
ということを頭に置いておいてください。
例えば、同じ Stream で二回 collect はできません。
Stream<String> s = names.stream();
List<String> a = s.filter(...).collect(Collectors.toList());
List<String> b = s.filter(...).collect(Collectors.toList()); // ここで IllegalStateException
Javaこれは、Stream が「一回きりの使い捨て」だからです。
必要なら、names.stream() から別の Stream をもう一度作る必要があります。
List の中身は「変換後の要素」であることを意識する
map と合わせて使うとき、
「どのタイミングで何に変わっているか」を意識すると、バグが減ります。
例えば、
List<Integer> lengths =
names.stream()
.map(String::length)
.collect(Collectors.toList());
Javaこの時点で List<Integer> になっています。
後続の処理でうっかり「文字列として扱ってしまう」などが起きないように、
「collect した結果の型」をしっかり意識しておくと安全です。
まとめ:Collectors.toList を自分の中でどう位置づけるか
Collectors.toList() を初心者向けに一言でまとめると、
「Stream で流した結果を、素直な List に戻すための、いちばん基本的な“集め方”」
です。
特に大事なのは、次の感覚です。
- Stream の途中までは
filter/mapなどで「流れの形」を作る - 最後に
collect(Collectors.toList())で「その流れを List に落とす」 - メソッドの戻り値や、他の API に渡すための“普通の List”が欲しくなったら
toListを思い出す
