Search code examples
javajava-stream

java lambda Transform a map<K, List<MyObject>> to map<K, List<String>>


I need to transform a map:

Map<String, List<Employee>> departmentWiseEmployees

to

Map<String, List<String>> departmentWiseEmployeeNames

...using Java lambda.

Mostly I see a 2nd level stream processing over the Map's entryset.

What would be an elegant way truly leveraging Java stream processing feature?


Solution

  • The canonical way is indeed to loop over/stream the entry set of the map:

    departmentWiseEmployees.entrySet()
        .stream()
        .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().stream().map(Employee::getName).toList()));
    

    The whole thing becomes easier to read if you extract a method to extract the names of the employees:

    departmentWiseEmployees.entrySet()
        .stream()
        .collect(Collectors.toMap(Map.Entry::getKey, e -> names(e.getValue())));
    
    static List<String> names(final Collection<? extends Employee> employees) {
      return employees.stream().map(Employee::getName).toList();
    }
    

    Alternatively, a method which accepts a map entry directly makes the stream over the map even more concise:

    departmentWiseEmployees.entrySet()
        .stream()
        .collect(Collectors.toMap(Map.Entry::getKey, YourClass::namesOfEmployees);
    
    static List<String> namesOfEmployees(final Map.Entry<?, ? extends Collection<? extends Employee>> entry) {
      return entry.getValue().stream().map(Employee::getName).toList();
    }
    

    Of course, both approaches can be combined:

    departmentWiseEmployees.entrySet()
        .stream()
        .collect(Collectors.toMap(Map.Entry::getKey, YourClass::namesOfEmployees);
    
    static List<String> namesOfEmployees(final Map.Entry<?, ? extends Collection<? extends Employee>> entry) {
      return names(entry.getValue());
    }
    
    static List<String> names(final Collection<? extends Employee> employees) {
      return employees.stream().map(Employee::getName).toList();
    }
    

    Going even further, you might want to provide a generic wrapper method instead to apply a method to the value of the map entry:

    departmentWiseEmployees.entrySet()
        .stream()
        .collect(Collectors.toMap(Map.Entry::getKey, fromValue(YourClass::names));
    
    static <V, R> Function<Map.Entry<?, ? extends V>, R> fromValue(final Function<? super V, ? extends R> fn) {
      return entry -> fn.apply(entry.getValue());
    }
    
    static List<String> names(final Collection<Employee> employees) {
      return employees.stream().map(Employee::getName).toList());
    }
    

    (completely untested, I hope I got the generics right)

    And ultimately, it's trivial to introduce a top-level method to transform the values of arbitrary maps:

    static <K, V, R> Map<K, R> mapValues(
        final Map<? extends K, ? extends V> map,
        final Function<? super V, ? extends R> fn) {
      return map.entrySet()
        .stream()
        .collect(Collectors.toMap(Map.Entry::getKey, fromValue(fn)));
    }
    
    final Map<String, List<String>> departmentWiseEmployeeNames
        = mapValues(departmentWiseEmployees, employees -> names(employees));
    // or with a method ref: mapValues(departmentWiseEmployees, YourClass::names);
    

    Because it was suggested in the comments: If you often have to handle maps with collections as values, it might make sense to create yet another method which doesn't apply a function to the value as a whole, but to each element in the value collection. An overload allows you to collect to different collection types, such as a set (defaults to producing a list):

    static <K, V, R> Map<K, List<R>> mapMultiValues(
        final Map<? extends K, ? extends Collection<? extends V>> map,
        final Function<? super V, ? extends R> fn) {
      return mapMultiValues(map, fn, Collectors.toList());
    }
    
    static <K, V, R, C extends Collection<? extends R>> Map<K, C> mapMultiValues(
        final Map<? extends K, ? extends Collection<? extends V>> map,
        final Function<? super V, ? extends R> fn,
        final Collector<R, ?, C> collector) {
      return mapValues(map, values -> values.stream().map(fn).collect(collector));
    }
    
    final Map<String, List<String>> departmentWiseEmployeeNames
        = mapMultiValues(departmentWiseEmployees, Employee::getName);
    
    final Map<String, Set<String>> departmentWiseEmployeeDistinctNames
        = mapMultiValues(departmentWiseEmployees, Employee::getName, Collectors.toSet());