【備忘録】ふとした時に使えそうな、JavaのストリームAPI関連

早速余談なのですが、1年前の記事では「STS4とYoutube Data API v3を使って簡単なwebアプリを作成する」といった内容の記事を作成しました。
👇️記事はこれです👇️

https://qiita.com/y_iwa/items/9e587aa6e91061ad3b80

しかし、全くというほど実用性はなく、今見返してみると労力に対して結果が見合ってなかったです・・・。当時は続きを書くつもりでしたが、手間がかかるため断念しました。

今回はちょっと趣向を変えて、実用性のある記事を作成したいな～と思ったので「汎用的に使えそうな、Javaの便利なストリームAPIに関する記事」を作成することにしました。
 
 またAPIという単語が出てきましたが、これは実際に現場でちょっと使ったものもあります。

# 1. なんとなく使っているけど、結局APIってなに
私は今の今まで勉強不足で、正直単語の意味については、すごいフワッとしたものしか知りませんでした（類似する単語が多すぎてイマイチ言語化が難しい）。
いい加減、言葉の意味くらいは知っておこうということで、今回のテーマでもある「JavaにおけるストリームAPI」の定義について、以下の記事を参考に軽く調べてみました。

https://data.wingarc.com/what-is-api-16084#API

「ライブラリAPIとは」という目次を参照すると、以下のような文章で説明されていました。
>Javaなどのプログラミングによる開発作業においては、クラスをまとめたクラスライブラリを用いることで、何百行にも及ぶコードを書くことなく、僅か数行で機能を実装することができます。

じゃあ、ライブラリとAPIはどう違うの？と思ったのですが、
広義ではニアリーイコールっぽい（？）です。

そもそもAPIの正式名称が「Application Programming Interface」なので、ここでいうAPIの意味とは、ライブラリ（ソースの集合体）を提供するためのインタフェース（接続口）を指す単語、ということになるのでしょうか・・・。

納得できそうで納得できない説明ですみません。
出来たらもうちょっと調べて補足します。

# 2. 実際にストリームAPIつかってみる
前置きが長くなってしまったので、ここから実際にJavaでどのようなメリットが有るのか、ソースの使用例をあげてなるべく簡潔に説明します。
大体SQLだったら一発で出来そうなことを、あえてJavaでやってしまう感じです。

### 2-1. パッケージ構造
```
パッケージ構成

qiita20241130
 ├─connection
 │ └─FileConnectionManager.java -> CSVファイルの中身からDTOクラスに値を設定
 ├─data
 │ └─employee.csv -> 従業員情報（テストデータ）
 ├─dto
 │ └─EmployeeDto.java -> 従業員情報 DTOクラス
 ├─main
 │ └─HogeController.java -> 今回メインとなる実行クラス
```
### 2-2. 前提となるクラス、テストデータ等準備
```Java:FileConnectionManager.java
package connection;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

import dto.EmployeeDto;

public class FileConnectionManager {
    public static List<EmployeeDto> doExecute() throws Exception {
        String username = System.getProperty("user.name");
        List<EmployeeDto> employeeList = new ArrayList<>();
        
        File file = new File("C:\\Users\\" + username + "\\Desktop\\qiita20241130\\data\\");
        File[] fileList = file.listFiles();

        // 複数ファイルある場合は全件数取得する
        for (File f : fileList) {
            // csvファイルのみに指定
            if (f.getName().substring(f.getName().lastIndexOf(".")).equals(".csv")) {
                try {
                    BufferedReader bfReader = new BufferedReader(new InputStreamReader(new FileInputStream(f), Charset.forName("SJIS")));
                    String readLineData = "";
                    boolean isFirstData = true;
                    while ((readLineData = bfReader.readLine()) != null) {
                        // 最初の行はスキップ
                        if (isFirstData) {
                            isFirstData = false;
                            continue;
                        }
                        String[] formatLineData =  readLineData.split(",");
                        EmployeeDto employee = new EmployeeDto();
                        employee.setDeptName(formatLineData[0]);
                        employee.setEmployeeName(formatLineData[1]);
                        employee.setAge(formatLineData[2]);
                        employee.setGender(formatLineData[3]);
                        employeeList.add(employee);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return employeeList;
    }
}
```
CSVファイルのテストデータに関しては、このサイトから生成してきました。

https://testdata.userlocal.jp/

```csvs:employee.csv
"deptName","employeeName","age","gender"
"人事部","Izumiyama Maiko",30,"女性"
"人事部","Tanaka Taro",43,"男性"
"営業部","Mikawa Mika",29,"女性"
"経理部","Yamada Hideki",42,"男性"
"経理部","Matoba Aiko",24,"女性"
"経理部","Sakoda Tomoaki",34,"男性"
"情報システム部","Nishimura Yuuichi",20,"男性"
"情報システム部","Tanaka Taro",52,"男性"
"情報システム部","Saida Madoka",39,"女性"
"情報システム部","Narita Chinatsu",28,"女性"
```
```Java:EmployeeDto.java
package dto;

public class EmployeeDto {

    /* 部門名 */
    private String deptName = "";
    
    /* 従業員の名前 */
    private String employeeName = "";

    /* 年齢 */
    private String age = "";

    /* 性別 */
    private String gender = "";

    public EmployeeDto() {
    }

    public String getDeptName() {
        return deptName;
    }
    public void setDeptName(String deptName) {
        this.deptName = deptName;
    }

    public String getEmployeeName() {
        return employeeName;
    }
    public void setEmployeeName(String employeeName) {
        this.employeeName = employeeName;
    }

    public String getAge() {
        return age;
    }
    public void setAge(String age) {
        this.age = age;
    }
    
    public String getGender() {
        return gender;
    }
    public void setGender(String gender) {
        this.gender = gender;
    }
}
```
### 2-3. 実際にかく
```Java:EmployeeDto.java
package main;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import java.util.stream.Collector;
import java.util.stream.Collectors;

import connection.FileConnectionManager;
import dto.EmployeeDto;

public class HogeController {
    public static void main(String[] args) throws Exception {
        // テストデータを取得する
        List<EmployeeDto> employeeList = FileConnectionManager.doExecute();

        // ここにコードを書く
    }
}
```

#### 2-3-1. 部門名のデータの種類だけ取得したいとき
下記のように記述するだけで、重複なしの部門名が取得できます。
SQLみたいにdistinctだけで弾いてくれるので、
組み合わせ次第でもっと無駄なコード省けそうです。
```Java:EmployeeDto.java
    List<String> test01Result =
            employeeList.stream()
                .map(obj -> obj.getDeptName())
                .distinct()
                .collect(Collectors.toList());
    /*
        出力結果
        "人事部"
        "営業部"
        "経理部"
        "情報システム部"
    */
```
#### 2-3-2. 部門名ごとのグループ分けされた全従業員のリストを取得したいとき
2-3-1の実装と合わせてみましたが、うーん、、、といった感じですね。
```Java:EmployeeDto.java
    // 2-3-1
    List<String> test01Result =
            employeeList.stream()
                .map(obj -> obj.getDeptName())
                .distinct()
                .collect(Collectors.toList());
    
    // 2-3-2
    List<List<EmployeeDto>> test02Result = new ArrayList<>();
    // 部門ごとにリストを作成
    for (String test01 : test01Result) {
        List<EmployeeDto> tempList = 
                employeeList.stream()
                    .filter(obj -> test01.equals(obj.getDeptName()))
                    .collect(Collectors.toList());
        test02Result.add(tempList);
    }
    // 出力のソースは省略
    /*
        出力結果（実際は部門名ごとにリストが作成されている）
        "人事部"
        "Izumiyama Maiko"
        30
        "女性"

        "人事部"
        "Tanaka Taro"
        43
        "男性"

        "営業部"
        "Mikawa Mika"
        29
        "女性"

        "経理部"
        "Yamada Hideki"
        42
        "男性"

        "経理部"
        "Matoba Aiko"
        24
        "女性"

        "経理部"
        "Sakoda Tomoaki"
        34
        "男性"

        "情報システム部"
        "Nishimura Yuuichi"
        20
        "男性"

        "情報システム部"
        "Tanaka Taro"
        52
        "男性"

        "情報システム部"
        "Saida Madoka"
        39
        "女性"

        "情報システム部"
        "Narita Chinatsu"
        28
        "女性"
    */
```
#### 2-3-3. 部門名ごとかつ、性別ごとに従業員名をグループ分けしたいとき
2-3-2の改良版です。
先程のはコードの記述が多い上にストリームAPIで書くお得感なかったですね。
下記のようにgroupingByを使えば、さっきのは3行で書けますね！
```Java:EmployeeDto.java
    // 2-3-3
    // これで既にMap<部門名,Map<性別,List<従業員情報>>>のリストが生成される
    Map<String,Map<String,List<EmployeeDto>>> test03Temp =
            employeeList.stream().collect(
                Collectors.groupingBy(EmployeeDto::getDeptName,
                Collectors.groupingBy(EmployeeDto::getGender)));
    // 👆️〇〇ごとに集計というのをやりたいのであれば、これだけで良いです。👆️

    // 何かしらの都合で、従業員情報ではなく従業員名のみを取得したい場合
    Map<String,Map<String,List<String>>> test03MapResult = new HashMap<>();
    for (Map.Entry<String,Map<String,List<EmployeeDto>>> entry1 : test03Temp.entrySet()) {
        Map<String,List<String>> entryToHashMap = new HashMap<>();
        for (Map.Entry<String,List<EmployeeDto>> entry2 : entry1.getValue().entrySet()) {
            // Map.Entry<String, List<EmployeeDto>> -> Map<String, Map<String, List<String>>> へ型を変換をする
            entryToHashMap.put(entry2.getKey(), entry2.getValue().stream().map(obj -> obj.getEmployeeName()).collect(Collectors.toList()));
            test03MapResult.put(entry1.getKey(), entryToHashMap);
        }
    }
    // 出力のソースは省略
    /*
        出力結果
        {
        "営業部"={
            "女性"=["Mikawa Mika"]}, 
        "情報システム部"={
            "男性"=["Nishimura Yuuichi", "Tanaka Taro"],
            "女性"=["Saida Madoka", "Narita Chinatsu"]},
        "人事部"={
            "男性"=["Tanaka Taro"],
            "女性"=["Izumiyama Maiko"]},
        "経理部"={
            "男性"=["Yamada Hideki", "Sakoda Tomoaki"],
            "女性"=["Matoba Aiko"]}
        }
    */
```
#### 2-3-4. 部門名ごとかつ、性別ごとの平均年齢を算出したいとき
groupingByのあとにaveragingIntと1行追加するだけで集計処理なんかができます。
ここまで来るといざという時ちょっと便利ですよね？
```Java:EmployeeDto.java
    Map<String,Map<String,Double>> test03MapResult =
            employeeList.stream().collect(
                Collectors.groupingBy(EmployeeDto::getDeptName,
                Collectors.groupingBy(EmployeeDto::getGender,
                Collectors.averagingInt((EmployeeDto e) -> Integer.parseInt(e.getAge())))));
    // 出力のソースは省略
    /*
        出力結果
        {
            "営業部"={"女性"=29.0}, 
            "情報システム部"={"男性"=36.0, "女性"=33.5}, 
            "人事部"={"男性"=43.0, "女性"=30.0}, 
            "経理部"={"男性"=38.0, "女性"=24.0}
        }
    */
```
#### 2-3-5. 独自の計算式から、部門名ごとかつ、性別ごとの平均年齢の値を算出したいとき
今回は例が悪くデタラメな意味のない計算式を作ってしまっているので、
あくまでご参考までに。
※ちなみにBigDecimalはdoubleやfloatのように丸め誤差が発生しない。
```Java:EmployeeDto.java
    // calcFunction（計算式）を作成
    Function<EmployeeDto, BigDecimal> calcFunction = employee -> new BigDecimal(Integer.parseInt(employee.getAge()))
            .add(new BigDecimal(10))      // 加算
            .subtract(new BigDecimal(5))  // 減算
            .multiply(new BigDecimal(4))  // 乗算
            .divide(new BigDecimal(2));   // 除算

    Map<String,Map<String,BigDecimal>> test03MapResult =
            employeeList.stream().collect(
                Collectors.groupingBy(EmployeeDto::getDeptName,
                Collectors.groupingBy(EmployeeDto::getGender,
                Collectors.reducing(BigDecimal.ZERO, calcFunction, BigDecimal::add)))); // calcFunctionの結果から、更に〇〇ごとに集計（加算）していっている
    // 出力のソースは省略
    /*
        出力結果
        {
            "営業部"={"女性"=68}, 
            "情報システム部"={"男性"=164, "女性"=154}, 
            "人事部"={"男性"=96, "女性"=70}, 
            "経理部"={"男性"=172, "女性"=58}
        }
    */
```