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?
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());