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:
- Having deeply nested maps and
- 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) * ]) * ] * */ } }
Advertisement
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)])