使用 Java 8 lambdas/transformations 合并并展平两个地图
Using Java 8 lambdas/transformations to combine and flatten two Maps
我有两张地图:
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
应该只有一条路径,尽管可能有 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 提供的出色解决方案,这可能效率较低,但我认为以这种方式编写和理解起来更容易。
我有两张地图:
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
应该只有一条路径,尽管可能有 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 提供的出色解决方案,这可能效率较低,但我认为以这种方式编写和理解起来更容易。