1. Stream APIとは
Stream APIとは、コレクションや配列の要素に対して、抽出・変換・集計などの処理を行うための仕組みです。(Java 8から導入)
例えば、数値のリストから10以上の値だけを取り出す場合、Stream APIでは次のように書けます。
List<Integer> numbers = List.of(5,10,15,20);
List<Integer> result = numbers.stream()
.filter(number -> number >= 10)
.toList();
実行結果
[10,15,20]
Streamはでーたそのものを保存する入れ物ではありません。
元のコレクションから要素を受け取り、その要素を順番に処理するための流れです。
List
↓
stream()
↓
filter()
↓
toList()
元のnumbersがStreamに変換され、filtersで条件に合う要素だけが残され、最後にtoListで新しいリストへ変換されます。
2. for文との違い
同じ処理を、通常のfor文で書くとこうなります。
List<Integer> numbers = List.of(5,10,15,20);
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (number >= 10) {
result.add(number);
}
}
Stream APIでは次のように書けます。
List<Integer> result = numbers.stream()
.filter(number -> number >= 10)
.toList();
for文は命令型と呼ばれ、処理手順(要素抽出→判定→追加)を順に書くのに対して
Stream APIは宣言型と呼ばれ、欲しい結果(10以上の数をリスト化)という目的を書きます。
for文:どう処理するかの手順
Stream API:何が欲しいかの結果
一概にStreamが万能なわけではなく、単純なループや複雑な条件分岐はfor文が適している場合もあります
3. Stream処理の基本構造
Stream APIは、基本的に「生成 → 中間操作 → 終端操作」の3段階で構成されます。
Streamの生成
↓
中間操作
↓
終端操作
次のcodeを 例に確認します。
List<String> result = names.stream()
.filter(name -> name.length() >= 5)
.map(name -> name.toUpperCase())
.toList();
-
Streamの生成
stream()を呼び出すことで、ListなどのコレクションからStreamを生成します。 -
主な中間操作: filter(抽出)、map(変換)、sorted(並び替え)、distinct(重複排除)、limit / skip(件数制御)
-
主な終端操作: toList / collect(コレクション変換)、count(要素数)、findFirst(先頭取得)、anyMatch(条件判定)、forEach(各要素への処理)
4. filter(条件に合う要素の抽出)
条件に合う要素(ラムダ式がtrueを返したもの)だけをStreamに残す中間操作。
- 関数型インタフェース:Predicate (引数を受け取り、booleanを返す)
// 10以上かつ偶数の要素だけを抽出する例
List<Integer> result = numbers.stream()
.filter(number -> number >= 10 && number % 2 == 0)
.toList();
// 事前に定義したPredicateを渡すことも可能
Predicate<Integer> condition = number -> number >= 10;
List<Integer> result = numbers.stream().filter(condition).toList();
5. map(要素の変換)
Stream内の要素を、別の値や方へ変換する中間操作。
- filterとの違い:filterは要素を「残すか除外するか」だが、mapは型そのものを変更する
- 関数型インタフェース:Function (Tを受け取りRを返す)
// メソッド参照を使い、ユーザーのリストから名前(String)のリストへ変換するサンプル
List<String> names = users.stream()
.map(User::getName)
.toList();
6. 終端操作での結果取得(toList・collect)
Streamはデータを保存しないため、最後にコレクションへ変換して結果を受け取ります。
- toList() (Java 16+):単純なList変換に最適。ただし、返されるListは変更不可
- collect() (Java8+):Collectors.toList()やtoSet()を使用。変更可能なList,SetやMapへの変換、グループ化などの複雑な集約に対応。
7. その他の便利な終端操作
- count():条件に合う要素数を返す(戻り値はlong型)
- findFirst():最初の要素を取得。該当要素がない可能性を考慮し、戻り値はOptionalになる。
◾️条件判定(Matchシリーズ):すべて、戻り値はboolean型 - anyMatch:1件でも条件を満たすか
- allMatch:すべて条件を満たすか
- noneMatch:1件も条件を満たさないか
8. ラムダ式・関数型インターフェースとの関係
Stream APIは、メソッドに対応する関数型インタフェースの処理を「ラムダ式」として受け取ります。
| Streamメソッド | 関数型インターフェース | 内部での役割 |
|---|---|---|
filter |
Predicate<T> |
test() メソッドで条件を判定 |
map |
Function<T, R> |
apply() メソッドで値を変換 |
forEach |
Consumer<T> |
accept() メソッドで値を受け取り処理 |
通常、test()やapply()を意識する必要はなく、ラムダ式を渡しておけば
Stream API側が内部で適切なタイミングで自動実行してくれます。
9. Stream APIの注意点
- 遅延評価:中間操作(filter内での出力など)は、toList()などの終端操作が呼ばれた瞬間に初めて実行される
- 再利用不可:終端操作を行なったStreamは閉じます。もう一度処理したい場合は、再度コレクションから新規Streamを生成する必要があります。
- 元のコレクションは変更されない:sortedやmapを行っても元のListは不変。Stream内で要素の状態を書き換える処理は、バグの原因になるため避けるべき
*適材適所でfor文を:複雑な分岐や break/ continue を含む処理は、for文の方が圧倒的に読みやすい。
10. 実務でよく見るStream処理パターン
// 有効なユーザーの名前のみを取得
List<String> activeUserNames = users.stream().filter(User::isActive).map(User::getName).toList();
// null要素を除外
List<String> result = values.stream().filter(Objects::nonNull).toList();
// 金額の合計を算出(プリミティブStreamへ変換)
int totalPrice = orders.stream.mapToInt(Order::getPrice).sum();
// 条件に合うデータが存在するか判定 / 最初の要素を取得
boolean existsAdmin = users.stream().anyMatch(User::isAdmin);
Optional<User> firstActive = users.stream().filter(User::isActive).findFirst();
10. まとめ
Stream APIとは、コレクションや配列の要素に対して、抽出・変換・集計などの処理を行うための仕組みです。
イメージとしては、データを「小川(Stream)」に流し、下流で回収するパイプライン構造として理解するとわかりやすいです
- Streamの生成:データが流れる川を作る
- 中間操作:流れたデータを濾過(filter)や変換(map)する関門を設置
- 終端操作:最下流にデータ回収の入れ物(List)を設置。この瞬間にデータが流し込まれ、最終データが取得できる(遅延評価)