Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Java 8 lambdas/transformations to combine and flatten two Maps

I have two maps:

  • Map<A, Collection<B>> mapAB
  • Map<B, Collection<C>> mapBC

I would like to transform them into a Map<A, Collection<C>> mapAC and I'm wondering if there's a smooth way to do that with lambdas and transformations. In my particular case, the collections are all sets, but I'd like to solve the problem for collections in general.

One thought I had was to first combine the two maps into a Map<A, Map<B, Collection<C>>> and then flatten it, but I'm open to any approach.

Data notes: B should only occur in the value collection associated with one A, and the same is true for mapBC (a given C is only mapped to from one B). As a result, there should only be one path from a given A to a given C, although there may be A -> B mappings for which there are no B -> C mappings and there may be B -> C mappings for which there are no corresponding A -> B mappings. These orphans simply don't appear in the resulting mapAC.

For the sake of comparison, here's an example of a purely imperative approach to the same problem:

Map<A, Collection<C>> mapAC = new HashMap<>();

for (Entry<A, Collection<B>> entry : mapAB.entrySet()) {
    Collection<C> cs = new HashSet<>();

    for (B b : entry.getValue()) {
        Collection<C> origCs = mapBC.get(b);
        if (origCs != null) {
            cs.addAll(origCs);
        }
    }

    if (!cs.isEmpty()) {
        mapAC.put(entry.getKey(), cs);
    }
}
like image 887
Matt Passell Avatar asked Jun 19 '15 19:06

Matt Passell


2 Answers

You didn't specify what you would want to do if some b from the first map don't exist in the second map, so this may not be exactly what you are looking for.

mapAB.entrySet().stream()
  .filter(e -> e.getValue().stream().anyMatch(mapBC::containsKey))
  .collect(toMap(
       Map.Entry::getKey,
       e->e.getValue().stream()
           .filter(mapBC::containsKey)
           .map(mapBC::get)
           .flatMap(Collection::stream)
           .collect(toList())
  ));
like image 137
Misha Avatar answered Sep 30 '22 13:09

Misha


I'm not a fan of the forEach approach, which is awkwardly imperative. A purer approach might be

mapAB.entrySet().stream()
  .flatMap(
      entryAB -> entryAB.getValue().stream().flatMap(
          b -> mapBC.getOrDefault(b, Collections.<C>emptyList())
             .stream().map(
                 c -> new AbstractMap.SimpleEntry<>(entryAB.getKey(), c))))
  // we now have a Stream<Entry<A, C>>
  .groupingBy(
     Entry::getKey,
     mapping(Entry::getValue, toList()));

...or maybe alternately

mapA.entrySet().stream()
  .flatMap(
      entryAB -> entryAB.getValue().stream().map(
          b -> new AbstractMap.SimpleEntry<>(
              entryAB.getKey(), 
              mapBC.getOrDefault(b, Collections.<C>emptyList()))))
  // we now have a Stream<Entry<A, Collection<C>>>
  .groupingBy(
     Entry::getKey,
     mapping(Entry::getValue, 
       reducing(
          Collections.<C>emptyList(),
          (cs1, cs2) -> {
             List<C> merged = new ArrayList<>(cs1);
             merged.addAll(cs2);
             return merged;
          })));
like image 32
Louis Wasserman Avatar answered Sep 30 '22 13:09

Louis Wasserman