javajava-streamgroupingby

Group a list of objects with multiple conditions


public class Student {
    String name;
    int age;
}

I have a list of Student objects and I need to group them according to specific logic:

  1. Group all students whose name starts with "A"
  2. Group all students whose name starts with "P"
  3. Group all students whose age is greater than or equal to 30

So far I what I have done:

List<Student> students = List.of(
     new Student("Alex", 31), 
     new Student("Peter", 33), 
     new Student("Antony", 32),
     new Student("Pope", 40), 
     new Student("Michel", 30));

Function<Student, String> checkFunction = e -> {
    if (e.getName().startsWith("A")) {
        return "A-List";
    } else if (e.getName().startsWith("P")) {
        return "P-List";
    } else if (e.getAge() >= 30) {
        return "30's-List";
    } else {
        return "Exception-List";
    }
};

Map<String, List<Student>> result = students.stream().collect(Collectors.groupingBy(checkFunction));

for (var entry : result.entrySet()) {
    System.out.println(entry.getKey() + "---");
    for (Student std : entry.getValue()) {
        System.out.println(std.getName());
    }
}

output

A-List---
Alex
Antony
P-List---
Peter
Pope
30's-List---
Michel

I understand this logic what I am following is wrong, that is why the 30's list is not populated correctly. Is it really possible with groupingBy()?


Solution

  • This can be handled like in Java 8 group by String but you will have to adapt checkFunction to actually return the groups of each Student.

    private Stream<String> mapToGroups(Student e) {
        Builder<String> builder = Stream.builder();
        boolean isException = false;
        if (e.getName().startsWith("A")) {
            builder.add("A-List");
        } else if (e.getName().startsWith("P")) {
            builder.add("P-List");
        } else {
            isException = true;
        }
        if (e.getAge() >= 30) {
            builder.add("30's-List");
        } else if (isException) {
            builder.add("Exception-List");
        }
        return builder.build();
    }
    

    However, if we were to use this function in a flatMap() call, we would loose the Student in the process. So what we really want is having this method return String<Map.Entry<String, Student>> so that we can later user the key for grouping and the value for collecting the groups:

    private Stream<Entry<String, Student>> mapToGroupEntries(Student e) {
        Builder<Entry<String, Student>> builder = Stream.builder();
        boolean isException = false;
        if (e.getName().startsWith("A")) {
            builder.add(new SimpleEntry<>("A-List", e));
        } else if (e.getName().startsWith("P")) {
            builder.add(new SimpleEntry<>("P-List", e));
        } else {
            isException = true;
        }
        if (e.getAge() >= 30) {
            builder.add(new SimpleEntry<>("30's-List", e));
        } else if (isException) {
            builder.add(new SimpleEntry<>("Exception-List", e));
        }
        return builder.build();
    }
    

    We can now use this function as part of a flatMap() call to convert our Stream<Student> to a Stream<Entry<String, Student>> and then group them:

    Map<String, List<Student>> result = students.stream()
            .flatMap(s -> mapToGroupEntries(s))
            .collect(Collectors.groupingBy(Entry::getKey,
                    Collectors.mapping(Entry::getValue, Collectors.toList())));