使用 Java 8 lambdas/transformations 合并并展平两个地图

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

我有两张地图:

我想将它们转换成 Map<A, Collection<C>> mapAC,我想知道是否有一种使用 lambda 和转换的平滑方法来做到这一点。在我的特殊情况下,集合都是集合,但我想解决一般集合的问题。

我曾想过先将两张地图组合成一个 Map<A, Map<B, Collection<C>>> 然后将其展平,但我对任何方法都持开放态度。

数据说明:B只应出现在与A关联的值集合中,mapBC也是如此(给定的C仅映射到一个 B)。因此,从给定的 A 到给定的 C 应该只有一条路径,尽管可能有 A -> B 映射但没有 B -> C 映射并且有可能是 B -> C 没有对应的 A -> B 映射的映射。这些孤儿根本不会出现在结果 mapAC.

为了便于比较,这里有一个针对同一问题的纯命令式方法的示例:

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);
    }
}

这个怎么样:

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

    mapAB.entrySet().stream().forEach(a -> {
        Collection<C> cs = new HashSet<>();
        a.getValue().stream().filter(b -> mapBC.containsKey(b)).forEach(b -> cs.addAll(mapBC.get(b)));
        mapAC.put(a.getKey(), cs);
    });

我不喜欢 forEach 方法,这种方法很笨拙。更纯粹的方法可能是

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

...或者交替

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;
          })));

如果第一个地图中的某些 b 在第二个地图中不存在,您没有指定要执行的操作,因此这可能不是您要查找的内容。

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())
  ));
Map<A, Collection<C>> mapC =
    mapA.entrySet().stream().collect(Collectors.toMap(
        entry -> entry.getKey(),
        entry -> entry.getValue().stream().flatMap(b -> mapB.get(b).stream())
            .collect(Collectors.toSet())));

请随意将 Collectors.toSet() 替换为 toList(), or even toCollection()

其实我并不反对命令式方法。由于您正在将它收集到内存中,因此真正使用 lambda 不会获得任何好处,除非它们会导致更清晰的代码。这里命令式的方式就好了:

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

for (A key : mapAB.keySet()) {
    Collection<C> cs = new HashSet<>();
    mapAC.put(key, cs);

    for (B b : mapAP.get(key)) {
        cs.addAll(mapBC.get(b)==null ?  Collections.emptyList() : mapBC.get(b));
    }
} 

尽管我已将您的 if 语句内联为三元运算符,而且我认为在 for 循环中使用键看起来更清晰。

My StreamEx library provides an EntryStream class 这是 Map.Entry 对象的流,带有一些额外的方便操作。这就是我使用我的库解决这个问题的方法:

Map<A, Collection<C>> mapAC = EntryStream.of(mapAB)
    .flatMapValues(Collection::stream) // flatten values: now elements are Entry<A, B>
    .mapValues(mapBC::get) // map only values: now elements are Entry<A, Collection<C>>
    .nonNullValues() // remove entries with null values
    .flatMapValues(Collection::stream) // flatten values again: now we have Entry<A, C>
    .groupingTo(HashSet::new); // group them to Map using HashSet as value collections

由于创建了更多的中间对象,作为@Misha 提供的出色解决方案,这可能效率较低,但我认为以这种方式编写和理解起来更容易。