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

Tags: , ,



I want to convert a nested map structure created by java streams + groupingBy into a list of POJOs, where each POJO represents one of the groups and also holds all the matching objects of that group.

I have the following code: I use project lombok for convenience here (@Builder, @Data). Please let me know if that is confusing.

My goal is to prevent two points from happening:

  1. Having deeply nested maps and
  2. As a result: Looping over these nested maps via keySets or entrySets to actually do stuff if the entries

Instead, I’d love a clean and flat list of POJOs that represent the grouping and conveniently hold the matching entries for each group.

Find the code on GitHub to run if locally, if you want.

Edit 1: I’ve updated the code again to remove the lastname and add another “Gerrit” object to have two objects with the same grouping. I hope this makes the intent clearer.

Edit 2: I have updated the code again to add a property on Person which is not part of the grouping.

I am looking for an output like this:

[
    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)
         *  ])  
         * ]
         *
         */
    }
}

Answer

I am not quite sure about the purpose of Grouping object because upon converting the maps to List<Grouping> the list of persons will actually contain duplicate persons.

This can be achieved with plain groupingBy person and converting the Map.Entry to Grouping.

Update
If the “key” part of Grouping has fewer fields than Person (favoriteColor has been added recently to Person), it’s worth to implement another POJO representing the key of Grouping:

@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);
    }
}

Then the instance of GroupingKey may be used in Grouping to avoid duplication.

Assuming that all-args constructor and a mapping constructor are implemented in 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());
    }
}

Then implementation could be as follows:

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

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

Output (test data changed slightly, key part is excluded):

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)])


Source: stackoverflow