JavaのStream API入門 - 高速で効率的なデータ処理を手に入れよう

## はじめに
Stream APIとはJava8から導入された強力な機能で、コレクション（ListとかMapとかSetとか）のデータ処理を効率的かつ簡潔に記述できるようになりました。
普段から使用しているStream APIですが備忘録も含め記事に残したいと思います。

Stream APIはラムダ式がよく出てきますがこの記事ではラムダ式の解説は行っていません。
ラムダ式については過去に記事を書いてますので気になる方はぜひ。

https://qiita.com/haraitai00/items/2f53867f0691e8ed9364


## Stream APIとは
Stream APIは、データ処理を効率的に行うためのフレームワークです。
**コレクションのデータを変換、フィルタリング、集計など、複数の操作を簡潔に記述できるようになります。** 
また、Stream APIは内部的に並列処理をサポートしており、マルチコアプロセッサーの恩恵を受けることができます。

## Stream APIの基本操作
Stream APIを使用するためには、まずコレクションからStreamオブジェクトを作成する必要があります。これは、コレクションのstream()メソッドを使って簡単に行えます。

```java
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
Stream<Integer> numberStream = numbers.stream();
```

## Stream APIの中間操作と終端操作
Stream APIでは、操作を **「中間操作」** と **「終端操作」** に分けています。
中間操作は、Streamを変換する操作で、終端操作は、Streamから結果を生成する操作です。

### 中間操作の例
`filter` ： 条件に一致する要素のみを抽出する
`map` ： 要素を別の型に変換する
`flatMap` ： 要素を別のStreamに変換し、それらのStreamを結合する

### 終端操作の例
`forEach` ： 各要素に対してアクションを実行する
`collect` ： 要素をコレクションに収集する
`reduce` ： 要素を結合して、単一の結果を生成する
`sum()` ： 数値要素の合計を求める
`count()` ： 要素の数をカウントする


## 実践例: Stream APIを使ったデータ処理色々

```java
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");

// 名前の長さが4文字以下のものだけを抽出し、大文字に変換してリストに収集
List<String> shortNames = names.stream()
                                // 名前の長さが4文字以下のものだけをフィルタリング(中間処理)
                                .filter(name -> name.length() <= 4)
                                // フィルタリングされた要素を大文字に置換(中間処理)
                                .map(String::toUpperCase)
                                // リスト化(終端処理)
                                .collect(Collectors.toList());

System.out.println(shortNames); // [ALICE, BOB, EVE]
```

```java
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // リストをStreamに変換
        // numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        int sum = numbers.stream()
                // 偶数だけをフィルタリング
                // filtered: [2, 4, 6, 8, 10]
                .filter(n -> n % 2 == 0)
                // 偶数を2倍にする
                // doubled: [4, 8, 12, 16, 20]
                .map(n -> n * 2)
                // 合計を計算
                .reduce(0, Integer::sum);

        System.out.println("合計: " + sum); // 合計: 60

```


### filter(フィルタリング)
```java
// 数値のリストを作成
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// 偶数のみをフィルタリングして新しいリストに収集
List<Integer> evenNumbers = numbers.stream()
                                   .filter(n -> n % 2 == 0) // 偶数かどうかチェック
                                   .collect(Collectors.toList());
```

### map(変換)
```java
// 名前のリストを作成
List<String> names = Arrays.asList("John", "Alice", "Bob", "Cathy");

// すべての名前を大文字に変換して新しいリストに収集
List<String> upperCaseNames = names.stream()
                                   .map(String::toUpperCase) // 大文字に変換
                                   .collect(Collectors.toList());
```

### sort(並び替え)
```java
// 名前のリストを自然順序でソートして新しいリストに収集
List<String> sortedNames = names.stream()
                                .sorted() // ソート
                                .collect(Collectors.toList());
```

### 集計 (リデュース)
```java
// 数値リストの合計を計算
int sum = numbers.stream()
                 .reduce(0, (a, b) -> a + b); // a は累積値, b は現在の要素。両方を足すことで合計を計算。
```

### 


## Stream APIの注意点と最適化
Stream APIを使用する際には、以下の注意点を押さえておくことが重要です。

**Streamは一度しか使用できません。** 再利用する場合は、新しいStreamを生成する必要があります。

Streamは遅延評価されます。中間操作は終端操作が実行されるまで適用されません。これにより、効率的なデータ処理が可能になります。
Stream APIは自動的に並列処理をサポートしていますが、parallelStream()を使用することで、明示的に並列処理を有効化することができます。ただし、並列処理を適用する場合は、スレッドセーフな操作であることを確認してください。
最適化については、以下のポイントを考慮してください。

短絡操作を利用して、不要な処理を省くことができます。例えば、anyMatch(Predicate<T> predicate)やfindFirst()などの操作は、必要最低限の要素を処理します。
Stream.iterate()やStream.generate()を使って無限のStreamを作成し、limit()で要素数を制限することができます。これにより、大規模なデータセットを効率的に処理できます。

## まとめ
Stream APIを使いこなしてコレクション操作をスッキリかけると気持ちがいいですね。

ちなみにKotlinにはStream APIと同様の機能がkotlin.collectionsに実装されています。
Kotlinに慣れるとJavaの`.stream()`でstream化するのが少し面倒になっちゃいました。。

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/