1

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:

  • Submap 1:
"key1", ["class_1", "Boy", "Jo"]
"key5", ["class_1", "Boy", "Oscar"]
  • Submap 2:
"key3", ["class_1", "Girl", "Amy"]
  • Submap 3:
"key6", ["class_2", "Boy", "Jimmy"]
  • Submap 4:
"key2", ["class_2", "Girl", "Alice"]
"key4", ["class_2", "Girl", "May"]

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

  • Submap 5:
"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
    ));

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
guiqin
  • 53
  • 4
  • Oops, thanks for catching it. I just corrected submap #5. The input is just a Map , the key is defined by caller (due to some business restriction), so goal is to group the students by class/gender/etc. and for each group it will be calling dedicated handler. Instead of simply filtering the illegal inputs, we also want to return all the failed instances to the caller. – guiqin Sep 14 '22 at 18:54
  • So my goal is to get all the submaps, which use the same structure as the input. I am just trying to partition my input. – guiqin Sep 14 '22 at 19:00
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/248049/discussion-between-alexander-ivanchenko-and-guiqin). – Alexander Ivanchenko Sep 14 '22 at 19:01

2 Answers2

1

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.

Alexander Ivanchenko
  • 25,667
  • 5
  • 22
  • 46
  • Awesome!! Thank you so much for the detailed explanation and for the example! I just tried and it works! I will look into how to get the error map. Thank you!! – guiqin Sep 14 '22 at 19:47
  • @guiqin Added a comment in regard to the handling of erroneous `Student` objects (see at the very end of the answer). – Alexander Ivanchenko Sep 14 '22 at 20:02
  • This post is helpful as I have exactly the same use case! But what if I want the result to be a nested map? E.g., I want to group the data map by gender, but I need to output it as nested map where the gender will be the key of the nested map, and the sub-map will be the value. How can I achieve it? – Green Ho Sep 15 '22 at 14:28
  • @GreenHo Hi, that doable, as well. You can use *nested collectors* to generate a nested map (the first collector would split the data-set based on the first property, and the downstream collector would deal with the second property). How it would look like depends on details of your problem. Comments are not well-suitable for answering questions. – Alexander Ivanchenko Sep 15 '22 at 15:16
  • @GreenHo If you're stuck with implementing it, I advise you to **open a question** and describe your problem carefully (don't forget to provide the **code attempt**, if you experience difficulties with stream-based solution, then at least try to solve it using loops, it always brings value to a question and folks are more eager to answer it, then "give-me-code" questions). Also, fill free to notify me by posting a comment with a link. – Alexander Ivanchenko Sep 15 '22 at 15:16
  • @AlexanderIvanchenko Thanks! I will try nested collectors as you suggested. – Green Ho Sep 15 '22 at 18:33
0

We can achieve that using Java 8 Streams groupingBy method. Following is the sample code

Map<String, List<Map.Entry<String, Student>>> filters = data.entrySet()
                .stream()
                .collect(Collectors.groupingBy(entry -> entry.getValue().getGradeAndClass()));

Output is

illegal class name=[key7=Student@2d98a335], 
class_2=[key2=Student@16b98e56, key6=Student@7ef20235, key4=Student@27d6c5e0, key8=Student@4f3f5b24]
class_1=[key1=Student@15aeb7ab, key5=Student@7b23ec81, key3=Student@6acbcfc0]}
Sagar Gandhi
  • 925
  • 6
  • 20