概要
ジェネリクスって便利ですよね。でも、記号(型引数)が多くて頭がごちゃごちゃしませんか?
APIをジェネリクス化するおすすめの手順は以下です。
- 切り出して汎用化したい処理を、まずは具体的な型のままで書く
- その処理の中で、型を可変にしたいものを洗い出す
- それらを型引数に置き換え、ジェネリクス化
具体例で見ましょう。
簡単な例
以下のようなコードがあります。
// In: List<SalesLine>
// Out: 商品コードをキー、販売数合計を値とするマップ
Map<String, Integer> map =
list.stream().collect(
Collectors.groupingBy(SalesLine::getProductCode,
Collectors.summingInt(SalesLine::getQuantity)
));
いま、「リストの要素をあるキーでグルーピングし、それぞれに対して何かの数値を合計してマップで返す」処理を、他でもよく使うのでユーティリティメソッドにしたいとしましょう。
いきなりジェネリクスで書き始めるのではなく、まずはベタに書きます。
public static Map<String, Integer> groupTotal(List<SalesLine> list) {
return list.stream().collect(
Collectors.groupingBy(SalesLine::getProductCode,
Collectors.summingInt(SalesLine::getQuantity)
));
}
次に、この処理の中で可変化したい型を洗い出します。
まず明らかなのはSalesLine
というリスト要素の型ですね。これをT
に置き換えます。(この時点ではコンパイルエラーとなります)
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
のままにします。
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
への変換であることを考えると、以下のようになります。
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)
));
}
以上で完成です。
利用側のプログラムは以下のようになります。
Map<String, Integer> map = groupTotal(list, SalesLine::getProductCode, SalesLine::getQuantity);
もう少し難しい例
以下のようなレポート出力プログラムを考えます。
ソート済みの売上明細リストを入力とし、商品コードが変わると小計行を出すという、業務アプリケーションにありがちな、いわゆるキーブレイク
処理です。
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円
さて、キーブレク処理は業務アプリケーションで頻出するので処理を汎用化したくなったとしましょう。
キーブレイク処理を行うクラスを、まずはベタに書きます。
処理フローのみを共通化し、具体的な出力ロジックは引数で渡すようにします。
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
に置き換えると以下のようになります。
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が完成。レポート出力プログラム側は以下のようになります。
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を見直すと、少し可読性が向上します。
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());
}
この実装の説明はブログの方に詳しく書きました。