使用Java 8 lambdas / transformations来组合和展平两个Maps

时间:2015-06-19 19:50:04

标签: java lambda java-8 java-stream

我有两张地图:

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

我想将它们转换为Map<A, Collection<C>> mapAC,我想知道是否有一种平滑的方式来做lambdas和变换。在我的特定情况下,集合都是集合,但我想解决集合中的问题。

我有一个想法是首先将两张地图合并为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);
    }
}

6 个答案:

答案 0 :(得分:3)

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

答案 1 :(得分:3)

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

答案 2 :(得分:1)

我的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提供的优秀解决方案可能效率较低,但我认为通过这种方式编写和理解起来更容易。

答案 3 :(得分:0)

这个怎么样:

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

答案 4 :(得分:0)

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

随意用https://gist.github.com/f72125cd4678069af7af.gittoList()替换Collectors.toSet()

答案 5 :(得分:0)

我实际上没有反对命令式方法。因为你正在将它收集到内存中,所以实际上使用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循环中的键看起来更清晰。