Java Stream APIのCollectionsユースケースまとめ
皆さん、こんにちは。LP開発グループのn-ozawanです。
12月頃から冬眠に入る熊ですが、同じクマ科のパンダは冬眠しません。パンダが食べる笹は一年中手に入るので、冬眠(エネルギーを節約)する必要がありません。また、笹は低カロリーなので冬眠に必要なエネルギーを確保できないのも理由の1つです。
本題です。
Stream APIのcollect()は、単にStreamをCollection型に変換してくれるだけでなく、集計やグルーピングなど多くの用途に対応しています。今回はそんなCollectionsクラスで提供されている、割と使うユースケースをまとめます。
目次
Collections ユースケース
Collectionに変換
StreamをCollection型に変換します。最も基本的な使い方です。
String[] names = {"佐藤", "鈴木", "高橋", "田中", "伊藤", "伊藤"};
// List型に変換
List<String> nameList = Arrays.stream(names).collect(Collectors.toList());
IO.println(nameList); // [佐藤, 鈴木, 高橋, 田中, 伊藤, 伊藤]
// Set型に変換
Set<String> nameSet = Arrays.stream(names).collect(Collectors.toSet());
IO.println(nameSet); // [佐藤, 鈴木, 高橋, 田中, 伊藤]
// 任意のCollection型に変換
Deque<String> nameDeque = Arrays.stream(names).collect(Collectors.toCollection(ArrayDeque::new));
IO.println(nameDeque); // [佐藤, 鈴木, 高橋, 田中, 伊藤, 伊藤]Map型に変換
StreamをMap型に変換します。IDなどの一意キーで、オブジェクトに効率よくアクセスしたい場合に利用します。
Person[] people = {
new Person("ID1", "佐藤"),
new Person("ID2", "鈴木"),
new Person("ID3", "高橋"),
new Person("ID4", "田中"),
new Person("ID5", "伊藤")
};
// IDをキー、Personオブジェクトを値とするMapに変換
Map<String, Person> idToPersonMap = Arrays.stream(people)
.collect(Collectors.toMap(Person::getId, person -> person));
IO.println(idToPersonMap); // {ID1=Person@1a2b3c4, ID2=Person@5d6e7f8, ID3=Person@9a0b1c2, ID4=Person@3d4e5f6, ID5=Person@7a8b9c0}
// IDをキー、名前を値とするMapに変換
Map<String, String> idToNameMap = Arrays.stream(people)
.collect(Collectors.toMap(Person::getId, Person::getName));
IO.println(idToNameMap); // {ID1=佐藤, ID2=鈴木, ID3=高橋, ID4=田中, ID5=伊藤}文字列結合
文字列を結合します。文字列以外のStreamを結合しようとするとエラーになりますので、map()などで文字列のStreamにしてから使います。
Person[] people = {
new Person("佐藤", 28),
new Person("鈴木", 34),
new Person("高橋", 22),
new Person("田中", 40),
new Person("伊藤", 30)
};
// 区切り文字なしで連結
String result1 = Arrays.stream(people).map(Person::getName)
.collect(Collectors.joining());
IO.println(result1); // 佐藤鈴木高橋田中伊藤
// 区切り文字ありで連結
String result2 = Arrays.stream(people).map(Person::getName)
.collect(Collectors.joining(", "));
IO.println(result2); // 佐藤, 鈴木, 高橋, 田中, 伊藤
// 接頭辞・接尾辞ありで連結
String result3 = Arrays.stream(people).map(Person::getName)
.collect(Collectors.joining(", ", "★開始★, ", ", ★終了★"));
IO.println(result3); // ★開始★, 佐藤, 鈴木, 高橋, 田中, 伊藤, ★終了★
数値集計・統計
数値の合計や平均などを求めます。
Person[] people = {
new Person("佐藤", 28),
new Person("鈴木", 34),
new Person("高橋", 22),
new Person("田中", 40),
new Person("伊藤", 30)
};
// 平均年齢
double averageAge = Arrays.stream(people)
.collect(Collectors.averagingInt(Person::getAge));
IO.println(averageAge); // 30.8
// 最年長
int maxAge = Arrays.stream(people)
.collect(Collectors.summarizingInt(Person::getAge))
.getMax();
IO.println(maxAge); // 40
// 一括集計 (個数、合計、最小、平均、最大)
IntSummaryStatistics ageStats = Arrays.stream(people)
.collect(Collectors.summarizingInt(Person::getAge));
IO.println(ageStats); // IntSummaryStatistics{count=5, sum=154, min=22, average=30.800000, max=40グルーピング
指定された要素ごとにグルーピングして、Map型として返却します。以下のソースコードは部署ごとにグルーピングした例です。Collectors.groupingBy()の第2引数に、先ほどの数値集計や統計を利用することで、部署ごとに計算することもできます。
Person[] people = {
new Person("佐藤", 28, "営業"),
new Person("鈴木", 34, "開発"),
new Person("高橋", 22, "営業"),
new Person("田中", 40, "開発"),
new Person("伊藤", 30, "総務")
};
// 部署ごとにグルーピング
Map<String, List<Person>> groupedByDepartment = Arrays.stream(people)
.collect(Collectors.groupingBy(Person::getDepartment));
IO.println(groupedByDepartment); // {営業=[Person@1a2b3c4, Person@5d6e7f8], 開発=[Person@9a0b1c2, Person@3d4e5f6], 総務=[Person@7a8b9c0]}
IO.println(groupedByDepartment.get("営業").get(0).getName()); // 佐藤
// 部署ごとに年齢の平均を算出
Map<String, Double> averageAgeByDepartment = Arrays.stream(people)
.collect(Collectors.groupingBy(
Person::getDepartment,
Collectors.averagingInt(Person::getAge)
));
IO.println(averageAgeByDepartment); // {営業=25.0, 開発=37.0, 総務=30.0}
// 部署ごとに最年長者を取得
Map<String, Optional<Person>> oldestByDepartment = Arrays.stream(people)
.collect(Collectors.groupingBy(
Person::getDepartment,
Collectors.maxBy(Comparator.comparingInt(Person::getAge))
));
IO.println(oldestByDepartment.get("開発").get().getName()); // 田中
マッピング、変換
Collectors.mapping()やCollectors.collectingAndThen()を使うことで、値や型変換をすることができます。これら単体で見ると、Stream.map()と同じように見えますが、Collectors.groupingBy()と合わせることで強力な処理ができるようになります。
Person[] people = {
new Person("佐藤", 28, "営業"),
new Person("鈴木", 34, "開発"),
new Person("高橋", 22, "営業"),
new Person("田中", 40, "開発"),
new Person("伊藤", 30, "総務")
};
// 部署ごとに名前のリストを取得
Map<String, List<String>> namesByDepartment = Arrays.stream(people)
.collect(Collectors.groupingBy(
Person::getDepartment,
Collectors.mapping(Person::getName, Collectors.toList())
));
IO.println(namesByDepartment); // {営業=[佐藤, 高橋], 開発=[鈴木, 田中], 総務=[伊藤]}
// 部署ごとに名前をカンマ区切りでグルーピング
Map<String, String> namesJoinedByDepartment = Arrays.stream(people)
.collect(Collectors.groupingBy(
Person::getDepartment,
Collectors.collectingAndThen(
Collectors.mapping(Person::getName, Collectors.toList()),
names -> String.join(", ", names)
)
));
IO.println(namesJoinedByDepartment); // {営業=佐藤, 高橋, 開発=鈴木, 田中, 総務=伊藤}
おわりに
グルーピングなどは集計処理などで使うことがあります。宣言的に記述することができるので、複雑な集計処理でも可読性高く実装できるのが魅力ですね。
ではまた。
