【Stream API】groupingBy と mapping でリストをマッピングしつつグルーピングする

エキサイト株式会社の西牧です。

groupingBy を使うとリストをグルーピングできますが、mapping もあわせて使うことでリストに何らかの処理をしつつグルーピングできることを新たに知ったので、その方法を紹介します。

想定ケース

以下のようなリストがあるとします。

@Value
class Row {
    long id;
    String name;
    String hobby;
}

Row row1 = new Row(1, "sato", "music");
Row row2 = new Row(1, "sato", "baseball");
Row row3 = new Row(2, "suzuki", "programming");
Row row4 = new Row(2, "suzuki", "game");
List<Row> rows = List.of(row1, row2, row3, row4);

このリストを UserDetail というクラスのリストに変換する、つまり、User ごとの Hobby リストを作るというのがやりたい処理です。

UserDetail(user=User(id=1, name=sato), hobby=[Hobby(value=music), Hobby(value=baseball)])
UserDetail(user=User(id=2, name=suzuki), hobby=[Hobby(value=programming), Hobby(value=game)])
@Value
class User {
    long id;
    String name;
}

@Value
class Hobby {
    String value;
}

@Value
class UserDetail {
    User user;
    List<Hobby> hobbies;
}

mapping を使わないコード

リストを任意のキーでグルーピングするのは、Stream API の collectgroupingBy を使うことでできます。

今回は User ごとにデータをグルーピングしたいので、User インスタンスをキーとしました。

groupingBy の第一引数がグルーピングした結果のキーです。

第二引数の LinkedHashMap::new はグルーピング後のリストの順序を保つためのものです。

第三引数でリストごとにグルーピングすることを指定しています。

本当は Hobby インスタンスのリストを作った上で、それらを User インスタンス でグルーピングしたかったのですが、その 2 つを同時に行う方法がわからず、こういう形になってしまいました。

LinkedHashMap<User, List<Row>> map = rows
        .stream()
        .collect(Collectors.groupingBy(e ->
                new User(e.getId(), e.getName()
                ), LinkedHashMap::new, Collectors.toList()));

List<UserDetail> userDetails = map.keySet()
        .stream()
        .map(e -> {
            List<Row> tmpRows = map.get(e);
            List<Hobby> hobbies = tmpRows
                    .stream()
                    .map(r -> new Hobby(r.getHobby()))
                    .collect(Collectors.toList());
            return new UserDetail(e, hobbies);
        }).collect(Collectors.toList());

mapping を使ったコード

後ほど上司の方に質問して、groupingBy の第三引数で Collectors.mapping を使うと、 「Hobby リストを作った上で、それらを User でグルーピングする」ことができるとわかり、修正したものが以下です。

Collectors.mapping(e -> new Hobby(e.getHobby()), Collectors.toList()) の部分で Hobby のリストを作っています。

LinkedHashMap<User, List<Hobby>> map = rows
        .stream()
        .collect(Collectors.groupingBy(e -> new User(e.getId(), e.getName()),
             LinkedHashMap::new,
                Collectors.mapping(e -> new Hobby(e.getHobby()), Collectors.toList())
        ));

List<UserDetail> userDetails = map.keySet()
        .stream()
        .map(e -> new UserDetail(e, map.get(e)))
        .collect(Collectors.toList());

おわりに

PHP の連想配列に慣れた身からすると、Java はリストの詰替えがやりづらいなと思っていたのですが、Stream API を使えば簡単にできました。

Stream API はまだ使いこなせていない部分もあるので色々試していきたいです。