javahashmapjava-streamcollectorsgroupingby

Java-Stream - How Collecor groupingBy() to split a Map into multiple Submaps


I have the following Student object:

public class Student {
    private String gradeAndClass;
    private String gender;
    private String name;

    // getters, constructor, etc.
}

In the original code, properties gradeAndClass and gender are enums, but for the purpose of simplicity let's consider them to be strings.

I have a map Map<String,Student>, where key is a unique string and value is a Student object.

I need this map to split into a bunch of maps Map<String,Student>, so the result should be a list of submaps List<Map<String,Student>>.

Let's consider the following example:

Map<String, Student> data = new HashMap<>();

data.put("key1", new Student("class_1", "Boy", "Jo"));
data.put("key2", new Student("class_2", "Girl", "Alice"));
data.put("key3", new Student("class_1", "Girl", "Amy"));
data.put("key4", new Student("class_2", "Girl", "May"));
data.put("key5", new Student("class_1", "Boy", "Oscar"));
data.put("key6", new Student("class_2", "Boy", "Jimmy"));
data.put("key7", new Student("illegal class name", "Boy", "err1"));
data.put("key8", new Student("class_2", "not supported", "err2"));

Is there an easy way to split the Map first by gradeAndClass then by gender, so that the result would be a list containing the following submaps:

"key1", ["class_1", "Boy", "Jo"]
"key5", ["class_1", "Boy", "Oscar"]
"key3", ["class_1", "Girl", "Amy"]
"key6", ["class_2", "Boy", "Jimmy"]
"key2", ["class_2", "Girl", "Alice"]
"key4", ["class_2", "Girl", "May"]

Also, I'd like to aggregate the illegal inputs to a separate map:

"key7", ["illegal class name", "boy", "err1"]
"key8", ["class_2", "not supported", "err2"]

I tried to filter data related to each submap separately (making use of the fact gradeAndClass and gender in the original code are well-defined enums), but it is very inefficient.

Basically, I've hard-coded all the condition checks and had to filter and regroup by each gradeAndClass and gender. I also had to reiterate each entry to get the illegal entries in order to put them to a separate "error map".

Sorry, I am very new to streams, so I know my solution is definitely not scalable, and thus I am looking for suggestion. Would it be possible to use stream groupingBy() to do all these?

Here's the code I've used to generate a separate submap:

Map<String, Student> submap = data.entrySet().stream()
    .filter(entry -> "class_1".equals(entry.getValue()) &&
                     (other conditions))
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        Map.Entry::getValue
    ));


Solution

  • To group students by grade and by gender, we need an object combining those two properties.

    For that, we can use Java 16 record, or a class for earlier JDK versions (alternatively there are few quick and dirty options like List<Object>, or concatenated String but it doesn't byes you anything, so I would not advise going this route).

    record Key(String gradeAndClass, String gender) {}
    

    As the first step, we need to create an auxiliary map which associates each Key (corresponding to a particular grade & gender) with a list of entries of the source-map (containing a string key and a student object). And that can be done using collector groupingBy().

    Then we need to transform each list in the auxiliary map into a Map<String,Student> and collect the result into a list.

    That's how it might be implemented:

    List<Map<String, Student>> result = data.entrySet().stream()
        .collect(Collectors.groupingBy(
            entry -> new Key(entry.getValue().getGradeAndClass(),
                             entry.getValue().getGender())
        ))
        .values().stream()
        .map(list -> list.stream()
            .collect(Collectors.toMap(
                Map.Entry::getKey, Map.Entry::getValue
            ))
        ).toList(); // or .collect(Collectors.toList()) for JDK versions earlier than 16
    

    In order to group entries containing Student object with incorrect value of grade or unspecified gender, you can implement a utility method that would, containing conditional logic for spotting such erroneous objects and returning a special instance of a Key (i.e. instantiated once and defined as static final field). If an object is valid an "normal" Key should be returned.

    When you have this method implemented just replace the classifier function of groupingBy() with a method reference which makes use of it.