APIをジェネリクス化するコツ

# 概要
ジェネリクスって便利ですよね。でも、記号（型引数）が多くて頭がごちゃごちゃしませんか？
APIをジェネリクス化するおすすめの手順は以下です。

1. 切り出して汎用化したい処理を、まずは具体的な型のままで書く
1. その処理の中で、型を可変にしたいものを洗い出す
1. それらを型引数に置き換え、ジェネリクス化

具体例で見ましょう。

# 簡単な例
以下のようなコードがあります。

```java
        // In: List<SalesLine>
        // Out: 商品コードをキー、販売数合計を値とするマップ
        Map<String, Integer> map =
                list.stream().collect(
                        Collectors.groupingBy(SalesLine::getProductCode,
                                Collectors.summingInt(SalesLine::getQuantity)
                        ));
```

いま、「リストの要素をあるキーでグルーピングし、それぞれに対して何かの数値を合計してマップで返す」処理を、他でもよく使うのでユーティリティメソッドにしたいとしましょう。  
いきなりジェネリクスで書き始めるのではなく、まずはベタに書きます。

```java
    public static Map<String, Integer> groupTotal(List<SalesLine> list) {
        return list.stream().collect(
                Collectors.groupingBy(SalesLine::getProductCode,
                        Collectors.summingInt(SalesLine::getQuantity)
                ));
    }
```

次に、この処理の中で可変化したい型を洗い出します。
まず明らかなのは`SalesLine`というリスト要素の型ですね。これを`T`に置き換えます。（この時点ではコンパイルエラーとなります）

```java
    public static <T> Map<String, Integer> groupTotal(List<T> list) {
        return list.stream().collect(
                Collectors.groupingBy(T::getProductCode,
                        Collectors.summingInt(T::getQuantity)
                ));
    }
```

次に、キーは文字列に限定せず`Integer`や独自型を使えるようにしたいので、`String`を`S`に置き換えます。マップの値の方は合計値計算を行うので`Integer`のままにします。

```java
    public static <S, T> Map<S, Integer> groupTotal(List<T> list) {
        return list.stream().collect(
                Collectors.groupingBy(T::getProductCode,
                        Collectors.summingInt(T::getQuantity)
                ));
    }
```

コンパイルエラーの原因となっている`T::getProductCode`と`T::getQuantity`を片付けましょう。
これらは、リスト要素型(`T`)のオブジェクトから値を抽出する処理を期待するので、関数型インタフェースで引数で渡すようにします。
それぞれ、`T->S`への変換、`T->Integer`への変換であることを考えると、以下のようになります。

```java
    public static <S, T> Map<S, Integer> groupTotal(
            List<T> list, Function<T, S> keyExtractor, Function<T, Integer> valueExtractor) {
        return list.stream().collect(
                Collectors.groupingBy(keyExtractor::apply,
                        Collectors.summingInt(valueExtractor::apply)
                ));
    }
```

以上で完成です。
利用側のプログラムは以下のようになります。

```java
        Map<String, Integer> map = groupTotal(list, SalesLine::getProductCode, SalesLine::getQuantity);
```

# もう少し難しい例
以下のようなレポート出力プログラムを考えます。
ソート済みの売上明細リストを入力とし、商品コードが変わると小計行を出すという、業務アプリケーションにありがちな、いわゆる`キーブレイク`処理です。

```java:ReportComponent.java
    public String outputReport(List<SalesLine> sales) {
        StringBuilder sb = new StringBuilder();

        String currentProductCode = null;
        int subtotalQty = 0;
        int subtotalAmount = 0;

        for (SalesLine sl: sales) {
            String productCode = sl.getProductCode();
            if (!productCode.equals(currentProductCode)) {
                // キーブレイク時は小計行を出力
                if (currentProductCode != null) {
                    sb.append(makeSubtotalLine(currentProductCode, subtotalQty, subtotalAmount)).append("\n");
                }
                currentProductCode = productCode;
                subtotalQty = 0;
                subtotalAmount = 0;
            }
            sb.append(makeNormalLine(sl)).append("\n");
            subtotalQty += sl.getQuantity();
            subtotalAmount += sl.getAmount();
        }
        // 最後の小計グループを処理
        sb.append(makeSubtotalLine(currentProductCode, subtotalQty, subtotalAmount)).append("\n");

        return sb.toString();
    }
```

このプログラムの出力例は以下です。

```
明細 2020-04-01 A 2個 200円
明細 2020-04-01 A 3個 300円
明細 2020-04-02 A 1個 100円
明細 2020-04-02 A 1個 100円
小計 A 7個 700円
明細 2020-04-01 B 1個 150円
明細 2020-04-02 B 2個 300円
明細 2020-04-02 B 2個 300円
小計 B 5個 750円
明細 2020-04-01 C 2個 400円
小計 C 2個 400円
```

さて、キーブレク処理は業務アプリケーションで頻出するので処理を汎用化したくなったとしましょう。  
キーブレイク処理を行うクラスを、まずはベタに書きます。
処理フローのみを共通化し、具体的な出力ロジックは引数で渡すようにします。

```java:KeyBreakProcessor.java
public class KeyBreakProcessor {

    private List<SalesLine> lines;

    public KeyBreakProcessor(List<SalesLine> lines) {
        this.lines = lines;
    }

    public void execute(Function<SalesLine, String> keyGenerator, Consumer<SalesLine> lineProcessor,
                        BiConsumer<String, List<SalesLine>> keyBreakLister) {
        String currentKey = null;
        List<SalesLine> subList = new ArrayList<>();
        for (SalesLine line : lines) {
            String key = keyGenerator.apply(line);
            if (!key.equals(currentKey)) {
                if (currentKey != null) {
                    keyBreakLister.accept(currentKey, subList);
                    subList = new ArrayList<>();
                }
                currentKey = key;
            }
            lineProcessor.accept(line);
            subList.add(line);
        }
        keyBreakLister.accept(currentKey, subList);
    }

}
```

この処理の中で可変化したい型を考えます。
明細行を表す`SalesLine`型と、ブレイクキーの`String`型が対象となりますので、それぞれ`L`、`K`に置き換えると以下のようになります。

```java:GeneralKeyBreakProcessor.java
public class GeneralKeyBreakProcessor<L, K> {

    private List<L> lines;

    public GeneralKeyBreakProcessor(List<L> lines) {
        this.lines = lines;
    }

    public void execute(Function<L, K> keyGenerator, Consumer<L> lineProcessor,
                        BiConsumer<K, List<L>> keyBreakLister) {
        K currentKey = null;
        List<L> subList = new ArrayList<>();
        for (L line: lines) {
            K key = keyGenerator.apply(line);
            if (!key.equals(currentKey)) {
                if (currentKey != null) {
                    keyBreakLister.accept(currentKey, subList);
                    subList = new ArrayList<>();
                }
                currentKey = key;
            }
            lineProcessor.accept(line);
            subList.add(line);
        }
        keyBreakLister.accept(currentKey, subList);
    }
}
```

これでジェネリックなAPIが完成。レポート出力プログラム側は以下のようになります。

```java:ReportComponent.java
    public String outputReportWithGenerics(List<SalesLine> sales) {
        GeneralKeyBreakProcessor<SalesLine, String> gkbp = new GeneralKeyBreakProcessor<>(sales);
        final StringBuilder sb = new StringBuilder();
        // キーの生成
        Function<SalesLine, String> keyGenerator = SalesLine::getProductCode;
        // 1明細行を処理して出力
        Consumer<SalesLine> processLine = sl -> sb.append(makeNormalLine(sl)).append("\n");
        // キーでグループされた明細行を処理し、小計を出力
        BiConsumer<String, List<SalesLine>> subTotal = (code, lines) -> {
            int qty = lines.stream().mapToInt(SalesLine::getQuantity).sum();
            int amount = lines.stream().mapToInt(SalesLine::getAmount).sum();
            sb.append(makeSubtotalLine(code, qty, amount)).append("\n");
        };
        gkbp.execute(keyGenerator, processLine, subTotal);

        return sb.toString();
    }
```

# まとめ
手順を踏めば、ジェネリクスを用いたAPIの実装は怖くないです！

# おまけ
ジェネリクス版のレポート出力プログラムですが、若干の読みづらさが残ります。
`流暢なAPI`のテクニックを用いてAPIを見直すと、少し可読性が向上します。

```java:ReportComponent.java
    public String outputReportWithFluent(List<SalesLine> sales) {
        FluentKeyBreakProcessor<SalesLine, String, String, String> processor = new FluentKeyBreakProcessor<>();
        List<String> groupList =
                processor.source(sales)
                .key(SalesLine::getProductCode)
                .eachLine(sl -> makeNormalLine(sl))
                .whenKeyChanged((key, list1, list2) -> {
                    String lines = list2.stream().collect(Collectors.joining("\n")) + "\n";
                    int qty = list1.stream().mapToInt(SalesLine::getQuantity).sum();
                    int amount = list1.stream().mapToInt(SalesLine::getAmount).sum();
                    return lines + makeSubtotalLine(key, qty, amount) + "\n";
                })
                .execute();

        return groupList.stream().collect(Collectors.joining());
    }
```

この実装の説明は[ブログ](https://blog.ynkb.xyz/168/)の方に詳しく書きました。