0

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)
         *  ])  
         * ]
         *
         */
    }
}
gerstams
  • 415
  • 2
  • 12
  • Please include all necessary code so it will compile. Your classes are incomplete. – WJS Mar 11 '21 at 19:43
  • By looking at the nested map solution it seems that a data base would be more appropriate. – WJS Mar 11 '21 at 19:52
  • Can you clarify the intend of your grouping? Is there any scenario where your Grouping has more than 1 Person in it? – Sorin Mar 11 '21 at 19:53
  • Yes, I wanted to keep the example short and thus there are never more than one person In the same group. But it could surely happen to have two objects in the same group, thus the list has > 1 entries – gerstams Mar 11 '21 at 19:55
  • @WJS I've updated my description with a link to the GitHub repo and also updated the code itself. Hope it helps – gerstams Mar 11 '21 at 19:57
  • @Sorin: I've updated my code example to hopefully make the intent clearer by adding another "Gerrit" object with identical properties – gerstams Mar 11 '21 at 20:03
  • @WJS A database is not suitable for us here as first we only have a few entries in the original persons list (100 - 200) and we need to to run as quickly as possible, thus I'd assume that pure Java is faster, right? – gerstams Mar 12 '21 at 07:41

2 Answers2

2

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)])
Nowhere Man
  • 19,170
  • 9
  • 17
  • 42
  • Thanks for the suggestion! :-) It's going into the direction I'm hoping for. Did you add the Gerrit + USA entry? If I have a nested grouping by using groupingBy more than once, how could I convert only the deepest grouping (Map) to a grouping POJO with your solution? – gerstams Mar 11 '21 at 20:51
  • Yes, I added that `Gerrit + USA`. There's no need to create intermediate maps using multiple `groupingBy` if you need to use the deepest grouping by all the properties of `Person` -- it's the same as if you grouped once by `Person`. – Nowhere Man Mar 11 '21 at 21:23
  • I just tried your solution. I think I've updated my code after your suggestion here, sorry. There are now properties on the objects which are not relevant for the grouping, thus the objects are not identical. – gerstams Mar 12 '21 at 07:37
  • What exactly happens when using the identity function (x -> x) in a groupingBy? Does it check whether X as a key is already present via the equals method of x's class? Or in a different way? If it is the equals method, than this might be difficult to do in my case as the favoriteColor is different on both objects (they are thus not equal), but should still be present in the same group – gerstams Mar 12 '21 at 07:39
  • 1. There were no `favoriteColor` in original question/code of `Person` class. 2. Yes, `x -> x` uses `equals()`. 3. It may be worth to implement a separate POJO representing the key value and then copy the key to `Grouping`, please check the update. – Nowhere Man Mar 12 '21 at 09:12
1

If you add a duplicate person in your solution, then grouping variable will hold only non-duplicate values. I believe it is not the right intention you want.

To achieve that you hold also duplicates, you can create a "grouping key" inside Person class to group values appropriately (or even use records, depending on your Java version).

    @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);
        });
    }
Alexander Gusev
  • 295
  • 1
  • 9
  • This looks interesting, thanks for sharing! However, what do you mean by grouping wont hold duplicates? Maybe my wording was wrong: I meant that both "Gerrit" objects will have the same grouping due to identical properties and thus end of in the same nested map. Right? – gerstams Mar 11 '21 at 20:35
  • Sure, you're welcome. I mean that your solution has an issue with duplicates. You currently have two "gerrit" objects. Your solution with nested groupings collects to map two entries which is correct, however in the collected map entry person list there is only one "gerrit" which is incorrect, I assume. There should be both "gerrit" objects, no? – Alexander Gusev Mar 11 '21 at 20:39
  • I want both my Gerrit objects in the same grouping, although they are not 100% identical (the properties relevant for the grouping are however identical) – gerstams Mar 12 '21 at 07:40