CodeGym /コース /JAVA 25 SELF /Stream API の基本操作: map, filter, collect

Stream API の基本操作: map, filter, collect

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

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種類に分かれます。

  • 中間操作(例: filtermapdistinct)— 処理の段階を記述します。新しいストリームを返しますが、それ自体では実行は開始されません。
  • 終端操作(例: collectforEachcount)— パイプラインを起動し、結果を生成します。

ストリームは「遅延評価」で動作します。終端操作が呼び出されるまで、何も計算されません。そのため、しばしば最後を 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]

手順:

  1. .stream() — リストからストリームを作成する。
  2. .filter(name -> name.startsWith("A")) — 「"A"」で始まる名前だけを残す。
  3. .map(String::length) — 各名前をその長さに変換する。
  4. .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 は出窓の猫のようにのんびりしています。終端操作(たとえば collectforEach)を呼ばない限り、何も起きません。もし 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"))
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION