Can I do this declaratively using groupingBy?

I have an Employee class:

public class Employee {

    public enum Sex {
        MALE, FEMALE
    }

    private final String name;
    private final Sex gender;
    private final LocalDate dateOfBirth;
    private final double salary;
    private final List<String> roles;

    // Constructor
    public Employee(String name, Sex gender, LocalDate dateOfBirth, double salary, List<String> roles) {
        this.name = name;
        this.gender = gender;
        this.dateOfBirth = dateOfBirth;
        this.salary = salary;
        this.roles = roles;
    }

    // Getters & toString omitted

    public static List<Employee> listOfStaff() {

        List<Employee> staff = new ArrayList<>();
        staff.add(new Employee("Jack", Employee.Sex.MALE,
                IsoChronology.INSTANCE.date(1954, 12, 2), 49999.00,
                new ArrayList<>(Arrays.asList("Manager", "Director"))));
        staff.add(new Employee("Jill", Employee.Sex.FEMALE,
                IsoChronology.INSTANCE.date(1995, 10, 25), 24999.00,
                new ArrayList<>(Arrays.asList("Secretary", "Manager", "Personnel"))));
        staff.add(new Employee("Dorothy", Employee.Sex.FEMALE,
                IsoChronology.INSTANCE.date(1972, 4, 7), 21999.00,
                new ArrayList<>(Arrays.asList("Secretary", "Receptionist"))));
        staff.add(new Employee("Bert", Employee.Sex.MALE,
                IsoChronology.INSTANCE.date(1968, 11, 5), 21999.00,
                new ArrayList<>(Arrays.asList("Clerk", "Receptionist"))));
        staff.add(new Employee("Mary", Employee.Sex.FEMALE,
                IsoChronology.INSTANCE.date(2001, 12, 3), 16999.00,
                new ArrayList<>(Arrays.asList("Trainee", "Receptionist"))));
        staff.add(new Employee("Matthew", Employee.Sex.MALE,
                IsoChronology.INSTANCE.date(1962, 4, 7), 12999.00,
                new ArrayList<>(Arrays.asList("Personnel", "Receptionist"))));

        return staff;
    }
}

I can extract a role and people within that role iteratively:

public class TestGrouping {
      /* Grouping streams */
      public static void main(String[] args) {

          Map<String, List<String>> roleAndNames = new HashMap<>();

          for (Employee e : Employee.listOfStaff()) {
              List<String> roles = e.getRoles();
              List<String> names = new ArrayList<>();
              names.add(e.getName());

              for (String r : roles) {
                  if(roleAndNames.get(r) == null) {
                      roleAndNames.put(r, names);
                  } else {
                      roleAndNames.get(r).add(e.getName());
                  }
              }
          }
          for (Map.Entry<String, List<String>> entry : roleAndNames.entrySet()) {
              System.out.println(entry.getKey() + ":" + entry.getValue().toString());
          }
      }
}

Which gives me the following output:

enter image description here

I have tried doing the same thing declaratively using streams but can’t seem to get the same results.

Thought I might be able to do this using groupingBy() and collectingAndThen() but no joy!

Is this possible?

Answer

Here is the solution step-by-step:

  1. You can use flatMap to get all the “role-name” possible pairs (Map.Entry<String, String>).
  2. Collect using Collectors.groupingBy into the Map structure using an additional Collectors.mapping downstream collector to extract the name of the flatmapped “role-name” pairs. It needs another one to packthese names into a List.
Map<String, List<String>> map = Employee.listOfStaff().stream()
        .flatMap(employee -> employee.getRoles()
                .stream()
                .map(role -> new AbstractMap.SimpleEntry<>(role, employee.getName())))
        .collect(Collectors.groupingBy(
                Entry::getKey,
                Collectors.mapping(Entry::getValue, Collectors.toList())));

map.forEach((k,v) -> System.out.println(k + ":" + v));

I see you use . If you later switch to a newer Java, you can use Map.entry(..) as of Java 9 instead of new AbstractMap.SimpleEntry(...).


Edit: If you don’t want to use , to fix your current iterative code, you need to either fix List insertion/update to the Map (both snippets work the same):

Map<String, List<String>> roleAndNames = new HashMap<>();
for (Employee employee: Employee.listOfStaff()) {
    for (String role: employee.getRoles()) {
        List<String> names = new ArrayList<>();
        if (roleAndNames.containsKey(role)) {
            names = roleAndNames.get(role);
        }
        names.add(employee.getName());
        roleAndNames.put(role, names);
    }
}
Map<String, List<String>> roleAndNames = new HashMap<>();
for (Employee employee: Employee.listOfStaff()) {
    for (String role: employee.getRoles()) {
        List<String> names = roleAndNames.getOrDefault(role, new ArrayList<>());
        names.add(employee.getName());
        roleAndNames.put(role, names);
    }
}

… or better use Map#computeIfAbsent which creates a new entry if there is not any AND always returns the value (which is a List, either an empty one OR with some names). So, you always use it to add the name in it with each iteration.

Map<String, List<String>> roleAndNames = new HashMap<>();
for (Employee employee: Employee.listOfStaff()) {
    for (String role: employee.getRoles()) {
        roleAndNames.computeIfAbsent(role, roleKey -> new ArrayList<>())
                    .add(employee.getName());
    }
}

That’s all 🙂 I love this method and its return type. It is very handy!

Leave a Reply

Your email address will not be published. Required fields are marked *