1. ストリームを作成する
Stream API を使うには、まずコレクションや配列からストリームを取得します。
ストリームの作成例
// リストから
List<String> names = List.of("Anna", "Boris", "Alex", "Alina");
Stream<String> stream = names.stream();
// 配列から
int[] numbers = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(numbers);
// 個々の値から
Stream<String> letters = Stream.of("A", "B", "C");
簡単にまとめると:
- list.stream() — コレクションから
- Arrays.stream(array) — 配列から
- Stream.of(...) — 個々の値から
アプリの文脈での例
たとえば、ユーザーのリストがあるとします:
List<String> users = List.of("Ivan", "Anna", "Petr", "Alexey");
Stream<String> userStream = users.stream();
中間操作と終端操作
重要なポイント: Stream API の操作は2種類に分かれます。
- 中間操作(例: filter、map、distinct)— 処理の段階を記述します。新しいストリームを返しますが、それ自体では実行は開始されません。
- 終端操作(例: collect、forEach、count)— パイプラインを起動し、結果を生成します。
ストリームは「遅延評価」で動作します。終端操作が呼び出されるまで、何も計算されません。そのため、しばしば最後を collect(...) で終わらせます。ここが、ストリームがコレクションや別の結果に戻る地点です。
2. filter 操作: 条件で要素を絞り込む
filter は中間操作で、指定した条件を満たす要素だけを通します。
シグネチャ
Stream<T> filter(Predicate<? super T> predicate);
Predicate は要素を受け取り、true(残す)か false(除外)を返す関数型インターフェイスです。
例: 偶数だけを残す
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);
List<Integer> evenNumbers = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evenNumbers); // [2, 4, 6]
何が起きているか?
- n -> n % 2 == 0 — ラムダ式で、その数が 2 で割り切れるかを判定しています。
- filter は偶数だけを残します。
例: 「A」で始まる名前をフィルタする
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");
List<String> aNames = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(aNames); // [Anna, Alex, Alina]
重要なポイント: filter はコレクションを変更しません。必要な要素だけを含む新しいストリームを作ります.
3. map 操作: 要素を別のものに変換する
map は変換の操作です。各要素に関数を適用し、新しい要素にして返します。
シグネチャ
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
Function は要素を受け取り、何か(別の型でも可)を返すインターフェイスです。
例: 文字列の長さを取得する
List<String> names = List.of("Anna", "Boris", "Alex");
List<Integer> nameLengths = names.stream()
.map(name -> name.length())
.collect(Collectors.toList());
System.out.println(nameLengths); // [4, 5, 4]
何が起きているか?
- map は文字列をその長さに変換しています(name -> name.length())。
- 結果は数値のストリームになります。
例: 文字列を大文字にそろえる
List<String> names = List.of("Anna", "Boris", "Alex");
List<String> upperNames = names.stream()
.map(name -> name.toUpperCase())
.collect(Collectors.toList());
System.out.println(upperNames); // [ANNA, BORIS, ALEX]
4. collect 操作: 結果をコレクションに集約する
collect は終端操作で、ストリーム処理を終えて結果をコレクションや他のコンテナに集めます。
シグネチャ
<R, A> R collect(Collector<? super T, A, R> collector)
難しそうなシグネチャに怖がらないでください。99% の場合は Collectors クラスの既製のコレクタを使います。
Collectors は「集め方」をまとめたユーティリティクラスです。結果をどの形にするか(リスト、セット、文字列など)をストリームに指示します。
例:
- Collectors.toList() — List にする
- Collectors.toSet() — Set にする
- Collectors.joining(", ") — カンマ区切りの文字列にする
つまり Collectors は、さまざまな形の箱のセットのようなもので、ストリームの要素をその箱に詰めていくイメージです。
例: 結果を List に集める
List<String> filtered = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
例: 結果を Set に集める
Set<String> uniqueNames = names.stream()
.map(String::toLowerCase)
.collect(Collectors.toSet());
例: 文字列をカンマで連結する
String result = names.stream()
.collect(Collectors.joining(", "));
System.out.println(result); // Anna, Boris, Alex
5. 操作の連鎖: フィルタ + 変換 + 集約
Stream API の最大の強みは、操作を次々につなげられることです。
例: 「A」で始まる名前の長さを取得する
List<String> names = List.of("Anna", "Boris", "Alex", "Alina", "Ivan");
List<Integer> aNameLengths = names.stream()
.filter(name -> name.startsWith("A"))
.map(String::length)
.collect(Collectors.toList());
System.out.println(aNameLengths); // [4, 4, 5]
手順:
- .stream() — リストからストリームを作成する。
- .filter(name -> name.startsWith("A")) — 「"A"」で始まる名前だけを残す。
- .map(String::length) — 各名前をその長さに変換する。
- .collect(Collectors.toList()) — 結果をリストに集約する。
同等の命令型コード
いわゆる「昔ながら」の書き方は次のとおりです:
List<Integer> result = new ArrayList<>();
for (String name : names) {
if (name.startsWith("A")) {
result.add(name.length());
}
}
比較してみてください: Stream API なら 1 行で、「どうやるか」ではなく「何をするか」として読めます。
6. 練習: 短い課題をいくつか
練習しましょう。すべての例は同じファイルで実行できます。データを差し替えるだけです。
課題 1: 奇数だけを残して二乗する
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7);
List<Integer> oddSquares = numbers.stream()
.filter(n -> n % 2 != 0)
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(oddSquares); // [1, 9, 25, 49]
課題 2: 文字列リストから各先頭文字のリストを作る
List<String> names = List.of("Anna", "Boris", "Alex");
List<Character> initials = names.stream()
.map(name -> name.charAt(0))
.collect(Collectors.toList());
System.out.println(initials); // [A, B, A]
課題 3: 長さが 3 より大きい文字列をフィルタし、Set に集める
List<String> words = List.of("cat", "dog", "elephant", "ant", "bear");
Set<String> longWords = words.stream()
.filter(word -> word.length() > 3)
.collect(Collectors.toSet());
System.out.println(longWords); // [bear, elephant]
7. filter、map、collect でよくあるミス
ミス 1: collect を忘れて結果が得られない
Stream API は出窓の猫のようにのんびりしています。終端操作(たとえば collect や forEach)を呼ばない限り、何も起きません。もし users.stream().filter(...).map(...); とだけ書いても、実行はされません。
ミス 2: filter と map の順序を取り違える
初心者は先に map、その後に filter をしてしまうことがあります。たとえば names.stream().map(String::length).filter(len -> len > 3) は、文字列ではなく数値になります。長さが 3 より大きい「文字列」が欲しいなら、先にフィルタし、その後に変換しましょう。
ミス 3: 変更されないことを忘れる
Stream API の操作は元のコレクションを変更しません。新しい結果を返すだけです。List<String> upper = names.stream().map(String::toUpperCase).collect(Collectors.toList()); の後でも、names コレクションはそのままです。
ミス 4: 外部の可変リストを使おうとする
次のように書くのは避けましょう:
List<String> result = new ArrayList<>();
names.stream().filter(...).forEach(name -> result.add(name));
collect を使う方が安全で、しかも短く書けます。
ミス 5: NullPointerException
コレクションに null が含まれる可能性がある場合、name.startsWith("A") を null に対して呼ぶと例外になります。可能なら null を弾くフィルタを追加しましょう:
.filter(name -> name != null && name.startsWith("A"))
GO TO FULL VERSION