将 streams.groupingBy() 的嵌套映射转换为 POJO 列表

Convert nested map of streams.groupingBy() into list of POJOs

我想将由 java streams + groupingBy 创建的嵌套映射结构转换为 POJO 列表,其中每个 POJO 代表其中一个组,并且还包含该组的所有匹配对象。

我有以下代码:为了方便,我在这里使用project lombok(@Builder、@Data)。如果这让我感到困惑,请告诉我。

我的目标是防止两点发生:

  1. 具有深度嵌套的地图和
  2. 因此:如果条目

相反,我想要一个简洁明了的 POJO 列表,它代表分组并方便地保存每个组的匹配条目。

如果需要,可以在 GitHub 到 运行 上找到代码。

编辑 1: 我再次更新了代码以删除姓氏并添加另一个“Gerrit”对象以使两个对象具有相同的分组。我希望这能让意图更清晰。

编辑 2: 我再次更新了代码以在不属于分组的 Person 上添加 属性。

我正在寻找这样的输出:

[
    Grouping(firstname=Jane, age=24, homeCountry=USA, persons=[Person(firstname=Jane, age=24, homeCountry=USA, favoriteColor=yellow)]),
    Grouping(firstname=gerrit, age=24, homeCountry=germany, persons=[
        Person(firstname=gerrit, age=24, homeCountry=germany, favoriteColor=blue), Person(firstname=gerrit, age=24, homeCountry=germany, favoriteColor=green)
    ])  
]
public class ConvertMapStreamToPojo {

    @Data
    @Builder
    static class Person {
        private String firstname;
        private int age;
        private String homeCountry;
        private String favoriteColor;
    }

    @Data
    static class Grouping {
        private String firstname;
        private int age;
        private String homeCountry;

        List<Person> persons;
    }

    public static void main(String[] args) {
        Person gerrit = Person.builder()
                .firstname("gerrit")
                .age(24)
                .homeCountry("germany")
                .favoriteColor("blue")
                .build();

        Person anotherGerrit = Person.builder()
                .firstname("gerrit")
                .age(24)
                .homeCountry("germany")
                .favoriteColor("green")
                .build();

        Person janeDoe = Person.builder()
                .firstname("Jane")
                .age(25)
                .homeCountry("USA")
                .favoriteColor("yellow")
                .build();

        List<Person> persons = Arrays.asList(gerrit, anotherGerrit, janeDoe);

        Map<String, Map<Integer, Map<String, List<Person>>>> nestedGroupings = persons.stream()
                .collect(
                        Collectors.groupingBy(Person::getFirstname,
                                Collectors.groupingBy(Person::getAge,
                                        Collectors.groupingBy(Person::getHomeCountry)
                                )
                        )
                );

        /**
         * Convert the nested maps into a List<Groupings> where each group
         * holds a list of all matching persons
         */
        List<Grouping> groupings = new ArrayList<>();
        for (Grouping grouping: groupings) {
            String message = String.format("Grouping for firstname %s age %s and country %s", grouping.getFirstname(), grouping.getAge(), grouping.getHomeCountry());
            System.out.println(message);

            System.out.println("Number of persons inside this grouping: " + grouping.getPersons().size());
        }

        // example groupings

        /**
         *
         * [
         *  Grouping(firstname=Jane, age=24, homeCountry=USA, persons=[Person(firstname=Jane, age=24, homeCountry=USA, favoriteColor=yellow)]),
         *  Grouping(firstname=gerrit, age=24, homeCountry=germany, persons=[
         *      Person(firstname=gerrit, age=24, homeCountry=germany, favoriteColor=blue), Person(firstname=gerrit, age=24, homeCountry=germany, favoriteColor=green)
         *  ])  
         * ]
         *
         */
    }
}

如果您在解决方案中添加重复的人,那么 grouping 变量将仅包含非重复值。我相信这不是你想要的正确意图。

为了实现您也持有重复项,您可以在 Person class 中创建一个“分组键”以适当地对值进行分组(甚至 use records,具体取决于您的 Java版本)。

    @Data
    @Builder
    static class Person {
        private String firstname;
        private String lastname;
        private int age;
        private String homeCountry;

        String groupingKey() {
            return firstname + "/" + lastname + "/" + age + "/" + homeCountry;
        }
    }

    @Data
    @Builder
    static class Grouping {

        private String firstname;
        private String lastname;
        private int age;
        private String homeCountry;

        List<Person> persons;
    }

    public static void main(String[] args) {
        Person johnDoe = Person.builder()
                .firstname("John")
                .lastname("Doe")
                .age(25)
                .homeCountry("USA")
                .build();

        Person janeDoe = Person.builder()
                .firstname("Jane")
                .lastname("Doe")
                .age(25)
                .homeCountry("USA")
                .build();

        Person duplicateJaneDoe = Person.builder()
                .firstname("Jane")
                .lastname("Doe")
                .age(25)
                .homeCountry("USA")
                .build();

        List<Person> persons = Arrays.asList(johnDoe, janeDoe, duplicateJaneDoe);

        Map<String, List<Person>> groupedPersons = persons.stream()
                .collect(Collectors.groupingBy(Person::groupingKey));

        // Since now you have grouped persons, you can go through map entries 
        // and fill your Grouping object list by splitting the key or just taking
        // the first object from the person list.

        List<Grouping> groupings = new ArrayList<>();

        groupedPersons.forEach((key, value) -> {
            String[] personBits = key.split("/");
            Grouping grouping = new Grouping.GroupingBuilder()
                    .firstname(personBits[0])
                    .lastname(personBits[1])
                    .age(Integer.parseInt(personBits[2]))
                    .homeCountry(personBits[3])
                    .persons(value)
                    .build();
            groupings.add(grouping);
        });
    }

我不太确定 Grouping 对象的用途,因为将地图转换为 List<Grouping> 人员列表实际上将包含重复人员。

这可以通过简单的 groupingBy 人并将 Map.Entry 转换为 Grouping 来实现。

更新
如果 Grouping 的“键”部分的字段少于 PersonfavoriteColor 最近已添加到 Person),则值得实现另一个代表 [的键的 POJO =14=]:

@Data
@AllArgsConstructor
static class GroupingKey {
    private String firstname;
    private int age;
    private String homeCountry;

    public GroupingKey(Person person) {
        this(person.firstname, person.age, person.homeCountry);
    }
}

然后 GroupingKey 的实例可以在 Grouping 中使用以避免重复。

假设在 Grouping

中实现了全参数构造函数和映射构造函数
@Data
@AllArgsConstructor
static class Grouping {
    // Not needed in toString, related fields are available in Person instances
    @ToString.Exclude
    private GroupingKey key;

    List<Person> persons;
    
    public Grouping(Map.Entry<GroupingKey, List<Person>> e) {
        this(e.getKey(), e.getValue());
    }
}

那么实现可以如下:

List<Grouping> groupings = persons.stream()
        .collect(Collectors.groupingBy(GroupingKey::new))
        .entrySet().stream()
        .map(Grouping::new)
        .collect(Collectors.toList());

groupings.forEach(System.out::println);

输出(测试数据略有改动,去掉关键部分):

Grouping(persons=[Person(firstname=Jane, age=24, homeCountry=USA, favoriteColor=Azure)])
Grouping(persons=[Person(firstname=gerrit, age=24, homeCountry=USA, favoriteColor=Red)])
Grouping(persons=[Person(firstname=gerrit, age=24, homeCountry=germany, favoriteColor=Black), Person(firstname=gerrit, age=24, homeCountry=germany, favoriteColor=Green)])