Streamとは？古いJavaでも新しく書こう～

世の中は~~古い~~地味だと思われるJavaですが、今の現場の金融機関ではまだ比較的に新しい。そのJavaの***比較的に新しい***特性について少し書くことが今度のネタです。

# Javaのバージョン事情
Javaの誕生は1995年、最新の長期サポート（LTS）バージョンは2023年9月にリリースされた***Java SE 21***です。長い時を渡り、時代と共に進化し続いてきたJavaですが、2024年に迎える現在、一番人気のあるバージョンは、なんと2014年3月がリリースされた***Java SE 8***です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2950721/8da37a52-78cd-fc3f-0810-6e4cb0aa4b5f.png)
> https://www.jetbrains.com/ja-jp/lp/devecosystem-2023/java/

JetBrainsのアンケート結果は上記の通りですし、この記事執筆した最初に、Qiitaのタグ候補もこの感じです。

<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2950721/11e9ab2d-ebfb-5521-29cf-98438c0e345c.png">

# Streamとは
では、この古くてもまだまだいけるJava8から追加されたイテレーションの拡張APIは、今回テーマのStreamです。

Streamは、日本語で「流れ」という意味になり、Javaで言うとデータの流れになります。要は工場のラインのようなものをイメージすると分かりやすいです。

<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2950721/d7ba1788-cc79-7909-5b6c-aeb8514f40f6.png">

入力データは、流れのように各段階で規定された処理だけが実行され、最終的に出力データは出来上がります。要約すると、Streamを使うには、3つのステップが必要です。
* Streamの生成(Collection)
* 中間操作(Intermediate)
* 終端操作(Terminal)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2950721/482ad0bd-a40e-176b-82f9-f8b3734507fd.png)

Streamを使えば、今まで手続き型言語の感じでデータを処理する作業は、あまりループを意識せずに、流れのように処理することができます。
```java
//20歳を超える利用者を抽出する
List<User> result=new ArrayList<>();
for(User user:users){

    if(user.getAge()>20){
        result.add(user);
    }
}
return result;

//Streamを使った実装
return users.stream().filter(s->s.getAge()>18).collect(Collectors.toList());
```

## Streamの生成

* 大体の ***Collection*** は ***.stream()*** があります
```java
Collection.stream()
Arrays.asList(1,2,3).stream()
Arrays.stream(new int[]{1,2,3})
```

* Steamの静的メソッドも使えます
```java
IntStream.range(int, int)
Stream.iterate(0, n -> n * 2)
Random.ints()
```

* ファイル操作(I/O)からもStreamが作れます
```java
java.nio.file.Files.walk()
java.io.BufferedReader.lines()
```

## Streamの中間操作

### filter
偶数だけを取得する
```java
List<Integer> l = IntStream.range(1,10)
        .filter( i -> i % 2 == 0)
        .boxed()
        .collect(Collectors.toList());
System.out.println(l); //[2, 4, 6, 8]
```

### map
文字要素をそれぞれハスキーコードに変換する
```java
List<Integer> l = Stream.of('a','b','c')
        .map( c -> c.hashCode())
        .collect(Collectors.toList());
System.out.println(l); //[97, 98, 99]
```

### limit
先頭要素だけ取得する
```java
List<Integer> l = IntStream.range(1,100).limit(5)
        .boxed()
        .collect(Collectors.toList());
System.out.println(l);//[1, 2, 3, 4, 5]
```

### peak
要素に対して***Consumer***を適用した後、次へ流します
```java
String[] arr = new String[]{"a","b","c","d"};
Arrays.stream(arr)
        .peek(System.out::println) //a,b,c,d
        .count();
```

## Streamの終端操作
中間操作と似ているメソッドが結構ありますが、違いはそのまま次へ流さず、そのまま戻り値を返却するところです。要はStreamの流れを終わらせるメソッドです。

### Match
***filter***の終端バージョン、判断結果をTrue/Falseで返却する
```java
System.out.println(Stream.of(1,2,3,4,5).allMatch( i -> i > 0)); //true
System.out.println(Stream.of(1,2,3,4,5).anyMatch( i -> i > 0)); //true
System.out.println(Stream.of(1,2,3,4,5).noneMatch( i -> i > 0)); //false
System.out.println(Stream.<Integer>empty().allMatch( i -> i > 0)); //true
System.out.println(Stream.<Integer>empty().anyMatch( i -> i > 0)); //false
System.out.println(Stream.<Integer>empty().noneMatch( i -> i > 0)); //true
```

### forEach
***peak***の終端バージョン
```java
Stream.of(1,2,3,4,5).forEach(System.out::println);
```

### reduce
要素を集約する、***iterate***の特性がある
```java
Optional<Integer> total = Stream.of(1,2,3,4,5).reduce( (x, y) -> x +y);
Integer total2 = Stream.of(1,2,3,4,5).reduce(0, (x, y) -> x +y);
```

### count
処理後の要素数を返却する
```java
System.out.println(IntStream.range(1,100).limit(5).count()); //5
```

### collect
Streamの結果をCollectorに格納するメソッドだが、2種類がある
```java
<R,A> R 	collect(Collector<? super T,A,R> collector)
<R> R 	collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
```
引数が1つしかないやつはCollectorの特性が分かればそのまま使える
```java
//年齢でgroupby
Map<Integer,List<User>> map= users.stream().
                                collect(Collectors.groupingBy(user::getAge));
```
引数が2つのやつは、概ねスレッド実行の時に、最後にそれぞれの結果を合併する必要がある場合が使われる
(容器生成するメソッド, 容器に格納するメソッド, 容器を合併するメソッド)
```java
List<String> asList = stringStream.collect(ArrayList::new, ArrayList::add,
                                           ArrayList::addAll);
String concat = stringStream.collect(StringBuilder::new, StringBuilder::append,
                                     StringBuilder::append)
                            .toString();
```

# 遅延評価
***遅延評価*** (lazy evaluation) は、よくHaskellの特徴として挙げられています。
```haskell
-- 先頭の10個だけが生成され
Prelude> take 10 [1..]
[1,2,3,4,5,6,7,8,9,10]

-- 先頭の4個だけが生成され
Prelude> zip [1..] ["a","b","c","d"]
[(1,"a"),(2,"b"),(3,"c"),(4,"d")]
```
上記のように、無限長リストの構文であっても、別に無限ループが実行されるわけではなく、終端操作(take zip)が必要な数だけは評価され、実行されました。

実はこの遅延評価はStreamの特徴でもあります。Streamは一連の操作の中、中間操作が呼び出されただけで、式として評価(実行)されません。終端操作が呼び出されてから、初めて式として評価され、実行されます。
```java
//先頭の10個だけが生成され
Stream.iterate(0, i -> ++ i).limit(10).forEach(System.out::print);

//終端操作がなければ何も実行されない
new Random().ints().limit(10).peek(System.out::println);
```

# 最後に
遅延評価ができる純粋関数型言語のHaskellはつい延々と長いコードになりがちですが、StreamはあくまでJava8の特性の一つです。ソースコードの可読性を注意しましょう。
```java
//良い例
users.stream().limit(3).
                sorted(Comparator.comparingInt(user::getAge)).
                peek(System.out::println).
                collect(Collectors.toList());

//悪い例
users.stream().limit(3).sorted(Comparator.comparingInt(user::getAge)).peek(System.out::println).collect(Collectors.toList());
```

# 参考
https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html