【Java】Stream APIを使ってグループごとの合計を求める
はじめに
Java のStream API
とCollectors
を使用し、データを指定の属性でグループ化し、各グループの合計を求める方法を説明します
Java デビュー戦の記録のためお手柔らかに、、、🐣
前提
商品データ
以下は例として使用する商品データです
カテゴリ | ブランド | 色 | ID | 商品名 | 価格 | 原価 |
---|---|---|---|---|---|---|
トップス | Lunaire | 白 | T001 | シルクブラウス | 49.99 | 25.00 |
トップス | Lunaire | 白 | T002 | レースタンクトップ | 29.99 | 15.00 |
トップス | Lunaire | 白 | T004 | カシミアセーター | 79.99 | 50.00 |
ジャケット | Alpine | グレー | J001 | ダウンジャケット | 149.99 | 90.00 |
パンツ | Axis | カーキ | P001 | チノパンツ | 59.99 | 35.00 |
パンツ | Axis | カーキ | P002 | スリムフィットジーンズ | 69.99 | 40.00 |
パンツ | Eclipse | ネイビー | P004 | ジョガーパンツ | 49.99 | 30.00 |
今回はこの商品データを「カテゴリ、ブランド、色」でグループ化を行い、以下のように合計を算出します。
カテゴリ | ブランド | 色 | ID | 商品名 | 価格 | 原価 |
---|---|---|---|---|---|---|
トップス | Lunaire | 白 | 159.97 | 90.00 | ||
ジャケット | Alpine | グレー | 149.99 | 90.00 | ||
パンツ | Axis | カーキ | 129.98 | 75.00 | ||
パンツ | Eclipse | ネイビー | 49.99 | 30.00 |
成果物
クラス定義
商品データとグループ毎の集計結果を扱うためのクラスを定義します
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
private static class ProductRecord {
private String productId;
private String category;
@Nullable
private String brand;
@Nullable
private String color;
private String name;
private Integer price;
private Integer cost;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE)
private static class Stat {
private Double price;
private Double cost;
}
商品レコードのリストを作成
final List<ProductRecord> recordList = Arrays.asList(
new ProductRecord("トップス", "Lunaire", "白", "T001", "シルクブラウス", 49.99, 25.00),
new ProductRecord("トップス", "Lunaire", "白", "T002", "レースタンクトップ", 29.99, 15.00),
new ProductRecord("トップス", "Lunaire", "白", "T004", "カシミアセーター", 79.99, 50.00),
new ProductRecord("ジャケット", "Alpine", "グレー", "J001", "ダウンジャケット", 149.99, 90.00),
new ProductRecord("パンツ", "Axis", "カーキ", "P001", "チノパンツ", 59.99, 35.00),
new ProductRecord("パンツ", "Axis", "カーキ", "P002", "スリムフィットジーンズ", 69.99, 40.00),
new ProductRecord("パンツ", "Eclipse", "ネイビー", "P004", "ジョガーパンツ", 49.99, 30.00)
)
グループ毎の集計
商品レコードを「カテゴリ、ブランド、色」でグループ化し、各グループの「価格」と「原価」の合計を求めます。
final Map<String, Map<String, Map<String, Stat>>> groupedMap = recordList.stream().collect(Collectors.groupingBy(
ProductRecord::getCategory,
Collectors.groupingBy(
record -> Optional.ofNullable(record.getBrand()).orElse(""),
Collectors.groupingBy(
record -> Optional.ofNullable(record.getColor()).orElse(""),
Collectors.reducing(
new Stat(),
record -> new Stat(record.getPrice(), record.getCost()),
(record, nextRecord) -> new Stat(
record.getPrice() + nextRecord.getPrice(),
record.getCost() + nextRecord.getCost()
)
)
)
)
));
集計結果のレコードを作成
グループ毎の合計を元に商品レコードを作成します
final List<ProductRecord> groupedRecordList = groupedMap.entrySet().stream()
.flatMap(categoryEntry -> categoryEntry.getValue().entrySet().stream()
.flatMap(brandEntry -> brandEntry.getValue().entrySet().stream()
.map(colorEntry -> ProductRecord.builder()
.productId(null)
.category(categoryEntry.getKey())
.brand(brandEntry.getKey())
.color(colorEntry.getKey())
.name(null)
.price(colorEntry.getValue().getPrice())
.cost(colorEntry.getValue().getCost())
.build()
)
)
).toList();
処理の流れ
- 商品レコードを「カテゴリ、ブランド、色」でグループ化
- グループ毎に「価格、原価」を合計
- グループ毎の合計を元に商品レコードを作成
Stream API とは
Java 8 で導入された機能で、コレクションや配列などの要素を高階関数的に扱うことが出来るものです
Stream API の概要
Stream を利用することで、一連のデータ処理を簡潔かつ直感的に記述できます。
Stream API は、以下の 3 つの主要な操作で構成されます:
- ソース: コレクションや配列から Stream を生成
- 中間操作: フィルタリングやマッピングなどの操作を行い、新しい Stream を返す
- 終端操作: 結果を生成する操作で、Stream の操作を終了する
List.stream().collect
collect()
メソッドは、ストリームの終端操作であり、ストリームの要素を収集してリストやマップなどの別のコレクションに変換します。
List<ProductRecord> productList = // ... 商品レコードのリスト
Stream<ProductRecord> productStream = productList.stream(); // リストをストリームに変換
List<ProductRecord> collectedList = productStream.collect(Collectors.toList()); // ストリームをリストに変換
Collectors.groupingBy
Collectors.groupingBy
は、ストリームの要素を指定したキーでグループ化するためのメソッドです。このメソッドは、グループ化の基準となる関数を受け取り、その基準に基づいて要素をマップに分割します。
Map<String, List<ProductRecord>> groupedByCategory = productList.stream()
.collect(Collectors.groupingBy(ProductRecord::getCategory));
Collectors.reducing
Collectors.reducing
は、ストリームの要素を集約するためのメソッドです。このメソッドは、初期値、集約関数、結合関数を受け取り、ストリームの要素を 1 つの結果にまとめます。
Map<String, Integer> totalPriceByCategory = productList.stream()
.collect(Collectors.groupingBy(
ProductRecord::getCategory,
Collectors.reducing(
0, //初期値
ProductRecord::getPrice, //集約関数
Integer::sum //結合関数
)
));
実装
ここからは、具体的な処理の流れに従って実装を行います
1. 商品レコードを「カテゴリ、ブランド、色」でグループ化
この部分では、商品データを「カテゴリ」「ブランド」「色」の 3 つの属性でネストされたマップにグループ化しています。
final Map<String, Map<String, Map<String, List<ProductRecord>>>> groupedMap = recordList.stream().collect(Collectors.groupingBy(
ProductRecord::getCategory,
Collectors.groupingBy(
record -> Optional.ofNullable(record.getBrand()).orElse(""),
Collectors.groupingBy(
record -> Optional.ofNullable(record.getColor()).orElse("")
)
)
));
1. ストリームの生成:
recordList.stream()
は、商品レコードのリストから Stream を作成します。
2. カテゴリでグループ化:
Collectors.groupingBy(ProductRecord::getCategory)
は、ストリームの要素をカテゴリ(getCategory)でグループ化します。
3. ブランドでネストされたグループ化:
Collectors.groupingBy(record -> Optional.ofNullable(record.getBrand()).orElse(""))
は、各カテゴリの中でさらにブランドでグループ化します。ブランドが null の場合は空文字列に置き換えます。
4. 色でさらにネストされたグループ化:
Collectors.groupingBy(record -> Optional.ofNullable(record.getColor()).orElse(""))
は、各ブランドの中でさらに色でグループ化します。色が null の場合は空文字列に置き換えます。
この結果、データはカテゴリ、ブランド、色の順にネストされたマップとして整理されます
2. グループ毎で「価格、原価」を合計
1 で作成したものに、グループごとに価格と原価を合計する処理を追加します
- final Map<String, Map<String, Map<String, List<ProductRecord>>>> groupedMap = recordList.stream().collect(Collectors.groupingBy(
+ final Map<String, Map<String, Map<String, Stat>>> groupedMap = recordList.stream().collect(Collectors.groupingBy(
ProductRecord::getCategory,
Collectors.groupingBy(
record -> Optional.ofNullable(record.getBrand()).orElse(""),
Collectors.groupingBy(
record -> Optional.ofNullable(record.getColor()).orElse(""),
+ Collectors.reducing(
+ new Stat(),
+ record ->
+ StatResult.builder()
+ .price(record.getPrice())
+ .cost(record.getCost())
+ .build(),
+ (record, nextRecord) ->
+ new StatResult(
+ record.getPrice() + nextRecord.getPrice(),
+ record.getCost() + nextRecord.getCost(),
+ )
+ )
)
)
));
価格と原価の合計:
-
Collectors.reducing
は、グループごとの要素を集約するために使用します。 -
new Stat(0, 0)
は、初期値として価格と原価が 0 の Stat オブジェクトを設定します。 -
record -> new Stat(record.getPrice(), record.getCost())
は、各商品レコードを Stat オブジェクトに変換します。 -
(stat1, stat2) -> new Stat(stat1.getPrice() + stat2.getPrice(), stat1.getCost() + stat2.getCost())
は、Stat オブジェクトを合計します。
この結果、各グループごとに価格と原価の合計を持つ Stat オブジェクトが作成されます。
3. グループ毎の合計を元に商品レコードを作成
この部分では、グループごとの合計を使って新しい商品レコードのリストを作成します。
final List<ProductRecord> groupedRecordList = groupedMap.entrySet().stream()
.flatMap(categoryEntry -> categoryEntry.getValue().entrySet().stream()
.flatMap(brandEntry -> brandEntry.getValue().entrySet().stream()
.map(colorEntry -> ProductRecord.builder()
.productId(null)
.category(categoryEntry.getKey())
.brand(brandEntry.getKey())
.color(colorEntry.getKey())
.name(null)
.price(colorEntry.getValue().getPrice())
.cost(colorEntry.getValue().getCost())
.build()
)
)
).collect(Collectors.toList());
1. 外側のエントリセットをストリームに変換:
-
groupedMap.entrySet().stream()
は、カテゴリごとのエントリセットをストリームに変換します。 -
.entrySet()
は、groupedMap
の各エントリをセット(キーと値のペア)として取得します。
2. 中間のエントリセットをストリームに変換:
categoryEntry.getValue().entrySet().stream()
は、各カテゴリごとのブランドエントリセットをストリームに変換します。
3. 内側のエントリセットをストリームに変換:
brandEntry.getValue().entrySet().stream()
は、各ブランドごとの色エントリセットをストリームに変換します。
4. 新しい商品レコードの作成:
-
ProductRecord.builder()
を使用して、新しい商品レコードを作成します。 -
categoryEntry.getKey()
,brandEntry.getKey()
,colorEntry.getKey()
は、それぞれのカテゴリ、ブランド、色のキーを取得します。 -
colorEntry.getValue().getPrice()
,colorEntry.getValue().getCost()
は、それぞれのグループの価格と原価の合計を取得します。
5. ストリームの収集:
.collect(Collectors.toList())
は、最終的に新しい商品レコードのリストにストリームの要素を収集します。
この結果、グループごとの合計を持つ新しい商品レコードのリストが作成されます。
Discussion