[Java11] Stream まとめ -Streamによるメリット-

- [Java11] Stream まとめ -Streamによるメリット-
- [[Java11] Stream 使い方まとめ -基本編-](https://qiita.com/nabekou29/items/e4f4c2afcdfa50346b34)

## Stream とは

> コレクションに対するマップ-リデュース変換など、要素のストリームに対する関数型の操作をサポートするクラスです。
引用元: [公式ドキュメント](https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/stream/package-summary.html#StreamOps) 

### まずは実際に見てみる

[公式リファレンス](https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/stream/Stream.html)より抜き出したものを一部改変

```java:stream.java
final int sum = widgets.stream()
                       .filter(w -> w.getColor() == RED)
                       .mapToInt(w -> w.getWeight())
                       .sum();
```

上の例では、`widgets`から `color`が`RED`のものを抽出して`weight`の値の合計を計算しています。
for文で書くと以下のようになります。

```java:for文.java
int sum = 0;
for (Widget w: widgets) {
    if (w.getColor() == RED) {
        sum += w.getWeight();
    }
}
```

## 何ができるの?

冒頭で書いた通りStreamとは「*コレクションに対するマップ-リデュース変換など、要素のストリームに対する関数型の操作をサポートするクラスです。*」

- コレクションに対するマップ-リデュース変換

ざっくり言うと**今までfor文でやっていたこと**、です。
上の合計値の計算然り、最大値の取得や新しいリストの作成などができます。

- 関数型の操作をサポートする

Streamクラスのメソッドの多くが関数型インターフェースを引数に取ります。例えばfilter関数には**どのようにフィルターをかけるか**、mapToInt関数には**どのように整数型に変換するか**を引数に渡します。
Streamには新しいStreamを返すメソッドがたくさん用意されており、そこに処理を渡すことで柔軟な処理が可能になっています。


## 何が良いの?

メリットとしては以下の 2 つが大きいと思います

1. 可読性の向上
1. 並列処理によるパフォーマンスの改善

### 可読性の向上

Streanを使った書き方が通常のコードと比べて**「宣言的」**だからです。
上の例の特に合計(`sum`)を求める部分に着目してください。

- Streamの場合
`sum()`を呼び出しているだけ。
データの集合に対して、**「合計値を求めてくれ」**と言っているだけです。

- for文の場合
`sum`の初期値を0とし各要素の値を加算している。最終的な値が合計値となる。
**「合計値の求める方法」**を実装しています。

for文の方はStreamの方と比べて**「命令的」**と呼びます。
「宣言的」な書き方では、細かい実装の隠蔽とコードの量の短縮ができます。やることを宣言しているだけなのでコードから**何をしたいかが理解しやすい**です。
逆に「命令的」な書き方は、やる内容を細かく記述するため処理が複雑になる程、理解が難しくなりバグの原因にもなります。

経験上、Streamを使う使わないによらず、1つのメソッドにおける命令的なコードは少ない方が、読みやすく安全なコードが書けます。


### 並列処理によるパフォーマンスの改善

こちらの恩恵は得られないケースの方が多いです。(経験上)
なぜなら、余程大量のデータを扱わないかぎりは**直列で実行する方が速い**ためです。

幸いStreanの実行を並列処理に変えるには`parallel()`呼び出すだけで済みます。
闇雲に並列処理にするのではなく、**データ量**、**順番が保持されないことによる問題**を考えて利用することで、並列処理の恩恵を最大限受けることができます。

以下の記事が参考になります。
参考: [ラムダ式で本領を発揮する関数型インターフェースとStream APIの基礎知識 (3/3)](https://www.atmarkit.co.jp/ait/articles/1404/30/news017_3.html)


## どうやって書くの?

Stream を使って処理を書くには**3 ステップ**あります。

1. Stream を作成する
1. 中間操作
1. 終端操作

例で示すと以下の通りです。

```java:stream.java
final int sum = widgets.stream() // Streamの作成
                       .filter(w -> w.getColor() == RED) // 中間操作
                       .mapToInt(w -> w.getWeight()) // 中間操作
                       .sum(); // 終端操作
```

### stream を作成する

ListやSetなどのCollectionから作成することが多いと思います。`widgets.stream()`がまさにそれです。その他、いくつかの値からStreamを作る方法、Stream.Builderを使う方法、などがあります。

### 中間操作

`filter` `mapToInt`が中間操作にあたります。各要素の値の変換や条件に基づいた要素の抽出などを行います。
`Stream<T>`や`IntStream`などのStreamを返します。Streamを返すためメソッドチェーンが可能です。
中間操作の処理は`fileter`などを呼び出したタイミングではなく。終端操作実行時に初めて実行されます。

<details><summary>実際に中間操作の実行タイミングがわかる例</summary><div>

```java
import java.util.*;
import java.util.stream.*;
import java.awt.*;

import static java.awt.Color.RED;
import static java.awt.Color.BLUE;

public class Main {
    public static void main(String[] args) throws Exception {
        List<Widget> widgets = List.of(new Widget(RED, 10), new Widget(BLUE, 20));
        
        Stream<Widget> stream1 = widgets.stream();
        Stream<Widget> stream2 = stream1.filter(w -> w.getColor() == RED);
        System.out.println("complete filtering");
        IntStream stream3 = stream2.mapToInt(w -> w.getWeight());
        System.out.println("complete mappint to integer");
        final int sum = stream3.sum();
    }
}

class Widget {
    private Color color;
    private int weight;
    
    public Widget(Color color, int weight) {
        this.color = color;
        this.weight = weight;
    }
    public Color getColor() {
        System.out.println(color);
        return color;
    }
    public int getWeight() {
        System.out.println(weight);
        return weight;
    }
}
```

```実行結果
complete filtering
complete mappint to integer
java.awt.Color[r=255,g=0,b=0]
10
java.awt.Color[r=0,g=0,b=255]
```

中間操作の直後には各処理が実行されていないことが見て取れます。
</div>

### 終端操作

`sum`が終端操作にあたります。これ以外には`collect` `findFirst`などがあります。合計値や新しいコレクションを返したりします。
中間操作と違い返す値は様々です。できることは多種多様に及びます。「合計値を求める」「新しいCollectionの生成」「各要素への処理(forEach)」などがあります。

中間操作と終端操作のできることは実例を示した方が良いと思うので次回以降に詳しくまとめたいと思います。

## 関数型インターフェース

今まで無視してきましたが、`w -> w.getColor() == RED` `w -> w.getWeight()`の存在に触れたいと思います。
この文法を**ラムダ式**と呼びますが、必ずしもラムダ式を書く必要はありません。
**ラムダ式は関数型インターフェースを実装するための1つの手段にすぎません。**

Streamのほとんどのメソッドが関数型インターフェースを引数にとるため、ここの理解はStreamの理解において避けては通れません。

正直、最初は雰囲気で書いてしまっても問題ないのでここから先は最悪理解できなくてもStreamを書くには問題ないかもしれないです。

### 関数型インターフェースとは

インターフェースの一種です。このインターフェースは**たった1つのメソッド**を持ちます
[Javaに標準で用意されているもの](https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/function/package-summary.html)のほか、自分で作成することもできます。

それぞれの型がわかりやすいように、最初の例をなるべく分割すると以下のようになります。

```java
Stream<Widget> stream1 = widgets.stream();
        
Predicate<Widget> predicate = w -> w.getColor() == RED;
Stream<Widget> stream2 = stream1.filter(predicate);
        
ToIntFunction<Widget> toIntFunction = w -> w.getWeight();
IntStream stream3 = stream2.mapToInt(toIntFunction);
        
final int sum = stream3.sum();
```
ラムダ式の代入先になっている`Predicate` `ToIntFunction`が関数型インターフェースです。`ToIntFunction`の定義は以下のようになっています。

```java:java.util.function.ToIntFunction.java
@FunctionalInterface
public interface ToIntFunction<T> {
    int applyAsInt​(T value);
}
```
関数型インターフェースには`@FunctionalInterface`を付与しますが、付けなくとも機能的には問題ありません。

### 実装の方法

Streamのメソッドに渡すために関数型インターフェースを実装したインスタンスを生成する必要がありますが、実装の方法は主に3通りあります。

- 匿名クラス
- ラムダ式
- メソッド参照

です。

では、それぞれの実装方法を比較してみましょう。
前提として、このような`Widget`クラスがあるものとします。

```java:Widget.java
class Widget {
    private Color color;
    private int weight;
    
    public Widget(Color color, int weight) {
        this.color = color;
        this.weight = weight;
    }
    
    public Color getColor() {
        return color;
    }
    
    public boolean isColorRed() {
        return color == RED;
    }
    
    public int getWeight() {
        return weight;
    }
}
```

#### 匿名クラス

とても読みやすいとは言えないです。

```java:匿名クラス.java
final int sum = widgets.stream()
                .filter(new Predicate<Widget>() {
                    public boolean test(Widget w) {
                        return w.isColorRed();
                    }
                }) 
                .mapToInt(new ToIntFunction<Widget>() {
                    public int applyAsInt(Widget w) {
                        return w.getWeight();
                    }
                }) 
                .sum();
```

<details><summary>一応これでもできます...</summary><div>

当然これでも動きます。

```java
static class WidgetTestColorIsRed implements Predicate<Widget> {
    public boolean test(Widget w) {
        return w.isColorRed();
    }
}
static class WidgetToWeightFunction implements ToIntFunction<Widget> {
    public int applyAsInt(Widget w) {
        return w.getWeight();
    }
}
```
```java
final int sum = widgets.stream()
                .filter(new WidgetTestColorIsRed())
                .mapToInt(new WidgetToWeightFunction())
                .sum();
```
まぁ、書けるだけで書くケースは全くと言っていいほどないと思います。

</div>

#### ラムダ式

圧倒的に読みやすくなりました。

```java:ラムダ式.java
final int sum = widgets.stream()
                .filter(w -> w.isColorRed()) 
                .mapToInt(w -> w.getWeight())
                .sum(); 
```
ラムダ式の書き方にも何通りかあります。

```java
// 引数なし
() -> "定数";

// 引数1個
n -> n + 1; // 括弧の省略が可能
(n) -> 2 * n;

// 引数２個以上
(a, b) -> Math.sqrt(a * a + b * b);
(x, y, z) -> x * y * z;

// 複数行
(a, b, c) -> {
     double s = (a + b + c) / 2;
     return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
```


#### メソッド参照

ここまで使ってきませんでしたが、**メソッド参照で書ける場合は積極的に使った方がコードが見やすくなります**。
`w`のような変数を置かないで済むのと、その時点でStreamの要素の型が何なのかが分かるので読みやすいです。

`クラス::メソッド`のように書きます。

```java:メソッド参照.java
final int sum = widgets.stream()
                .filter(Widget::isColorRed)
                .mapToInt(Widget::getWeight)
                .sum();
```


匿名クラスもラムダ式もメソッド参照も関数型インターフェースのインスタンスを生成するための方法です。自分で定義した関数型インターフェースであっても同様の方法でインスタンスの作成が可能です。
関数型インターフェースでは、たった1つのメソッドを実装すればいいことがわかっているため、ラムダ式のような型の情報が全くない書き方でも推論でどうにかなるわけです。

## おわりに

次回、Streamの具体的な使い方を紹介したいと思います。

## 参考
- [公式リファレンス](https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/stream/Stream.html)
- [宣言的？ Declarative?どういうこと？](https://qiita.com/Hiroyuki_OSAKI/items/f3f88ae535550e95389d)
- [ラムダ式で本領を発揮する関数型インターフェースとStream APIの基礎知識 (3/3)](https://www.atmarkit.co.jp/ait/articles/1404/30/news017_3.html)
- [関数型プログラミングって何、ラムダってなんだよ](https://qiita.com/lrf141/items/98ffbeaee42d30cca4dc)
- [ラムダ式とStream APIで学ぶモダンJava ― 関数型を取り入れて変化するJava言語の現在](https://employment.en-japan.com/engineerhub/entry/2019/04/25/103000)