【问题标题】:Using Java 8 lambdas/transformations to combine and flatten two Maps使用 Java 8 lambdas/transformations 组合和展平两个 Map
【发布时间】:2015-06-19 19:50:04
【问题描述】:

我有两张地图:

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

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

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

数据说明:B 应该只出现在与一个A 关联的值集合中,mapBC 也是如此(给定的C 仅映射到一个B)。因此,从给定的A 到给定的C 应该只有一条路径,尽管可能存在没有B -> C 映射的A -> B 映射,并且可能存在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);
    }
}

【问题讨论】:

  • 你想要一个没有中间列的两个一对多关系的连接吗?
  • @MikeSamuel 是的,这绝对是一种看待它的方式。
  • 您能添加数据示例吗?例如,如果我们有Map&lt;Person, Set&lt;Job&gt;&gt;Map&lt;Job, Set&lt;Tool&gt;&gt;,不同的人是否有可能拥有相同的工作,或者少数工作使用相同的工具?那么像p1 -&gt; {j1, j2}, p2-&gt;{j2, j3} 这样的可能吗?还有job1-&gt;{tool1, tool2} job2-&gt;{tool2, tool3} job3-&gt;{tool4}?你期待什么结果?您是否还希望 Collection&lt;X&gt; 成为 Set 或者元素可以多次存在于其中?
  • @Pshemo 请参阅问题中新的“数据说明”部分。

标签: java lambda java-8 java-stream


【解决方案1】:

如果第一张地图中的某些 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())
  ));

【讨论】:

  • 顺便提一下,您可能想提一下代码假定您已静态导入Collectors.toMap()Collectors.toList()。我很高兴假设您还导入了 Map.Entry,因此您也可以删除 Map. 前缀。
  • 我本来打算接受这个答案,但后来我写了一个单元测试,发现如果 mapAB 中的一个条目指向一个 B 集合,那么这些 B 都不是 mapBC 中的键,你结束在 mapAC 中添加一个指向空集合的条目。这不是世界末日,但我希望在这种情况下没有条目。
  • 在外部收集之前添加以下过滤器可以解决问题,但它相当难看:filter(e -&gt; !e.getValue().stream().filter(mapBC::containsKey). collect(Collectors.toSet()).isEmpty())
  • 或者,更简单地说,e-&gt;e.getValue().stream().anyMatch(mapBC::containsKey)
【解决方案2】:

我不喜欢 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;
          })));

【讨论】:

  • 我正在考虑你的答案。第二个版本看起来很聪明,但有点难以解释。顺便说一句,我已经给出了地图名称(mapAB、mapBC)。我会编辑你的答案以使用 mapAB,但它只涉及更改 StackOverflow 不允许我这样做的足够少的字符。
  • 更新了变量名称以及您指出mapBC 可能不包含出现在mapAB 中的所有B。
【解决方案3】:

我的StreamEx 库提供了一个EntryStream 类,它是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 提供的出色解决方案效率较低,但在我看来,这种方式更容易编写和理解。

【讨论】:

    【解决方案4】:
    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(),甚至toCollection()

    【讨论】:

    • 我喜欢这种方法的清晰性,但请参阅上面的问题更新。 mapB.get(b) 可能返回 null,所以你必须处理它。另请参阅我更新的变量命名。
    【解决方案5】:

    我实际上并不反对命令式方法。由于您将它收集到内存中,因此使用 lambdas 真的没有任何好处,除非它们导致代码更清晰。这里命令式的方法很好:

    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 循环中使用键看起来更清晰。

    【讨论】:

    • mapAB 和 mapBC 最终可能会非常庞大​​,所以我希望尽可能避免额外的查找。遗憾的是 Java 没有 Groovy's Elvis operator,因为它可以完美地替代您的三元表达式,同时避免双重查找。
    【解决方案6】:

    这个怎么样:

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

    【讨论】:

    • 这绝对比我刚刚添加到问题中的命令式等价物更紧凑,但它确实有一个缺陷,我最初在我的示例中也未能处理。您的电话 mapB.get(b)(您可以将其更新为 mapBC - 请参阅上面的命名更新)可能会返回 null,因此您需要处理它。
    • 你是对的,请检查我的编辑...(添加过滤器)
    • 感谢您的编辑。不幸的是,它仍然表现出与 Misha 的答案相同的行为,为在 mapBC 中没有映射的 B 创建空集合。有关更多详细信息,请参阅该答案的 cmets。
    猜你喜欢
    • 1970-01-01
    • 2014-05-27
    • 1970-01-01
    • 2011-10-07
    • 2021-01-27
    • 1970-01-01
    • 2017-03-02
    • 1970-01-01
    • 2022-07-05
    相关资源
    最近更新 更多