假设我有一个 HashMap,其中包含作为字符串的键和作为整数集的值(Map
Map<String, Set<Integer>> map = new HashMap<>();
map.put("w1", Set.of(1,3,4,6,7));
map.put("w2", Set.of(2,3,4,5,7));
map.put("w3", Set.of(1,2,3,5,7));
如何使用 Java 中的 Streams 找到所有键的通用值集?例如:在这种情况下,所有键的公共值集是 Set.of(3,7)
首先请注意,使用流并不总是最干净的方式。
我的想法是获取第一组并迭代其余组以检查它们是否全部包含它:
Set<Integer> res = map.values().iterator().next().stream()
.filter(item -> map.values().stream().allMatch(set -> set.contains(item)))
.collect(Collectors.toSet());
这是一个简洁的解决方案,但它会检查第一组两次。您还可以添加检查以查看地图是否包含任何条目。
Set<Integer> commonValues(Map<String, Set<Integer>> map) {
if (map.isEmpty()) {
return new HashSet<>();
}
Set<Integer> intersection = new HashSet<>(map.values().iterator().next());
map.values().forEach(v -> intersection.retainAll(v));
return intersection;
}
我的方法是首先将不同的值分组并进行计数。然后只保留那些计数等于映射中条目数的条目。这是可行的,因为每个集合只能包含该值一次。
map.values().stream().flatMap(Set::stream)
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream().filter(e -> e.getValue() == map.size())
.map(Map.Entry::getKey).collect(Collectors.toSet());
这可以通过利用
collect()
操作来完成。
此方法背后的逻辑是从值集合中获取随机集,用一个新集包装它以避免突变,然后使用retainlAll()
方法将所有
集组合在一起一个。
即使值由不可变集表示(如问题中的示例),这也不是问题,因为它们将被完整保留。唯一会发生突变的集合是由collect()
内的supplier函数提供的
新集合。
如果地图空,可能会出现问题。
在这种情况下,任何从values集合中获取first set的尝试都会失败。因此,这种情况必须单独处理,如下面的方法
getFirst()
所示。
它可能看起来像:
public static Set<Integer> getIntersectionStream(Map<String, Set<Integer>> map) {
return map.values().stream()
.collect(() -> getFirst(map),
Set::retainAll,
Set::addAll);
}
相同的逻辑可以迭代实现:
public static Set<Integer> getIntersectionLoop(Map<String, Set<Integer>> map) {
Set<Integer> intersection = new HashSet<>(getFirst(map));
for (Set<Integer> next: map.values()) {
intersection.retainAll(next);
}
return intersection;
}
方法 get
getFirst()
负责从 values 集合中检索 random set。在空映射的情况下,它将返回一个空的不可修改的集合,否则,它将产生流返回的第一个集合。
注意,在这种情况下,在可选项上调用
get()
是安全的,因为我们期望结果出现。
public static Set<Integer> getFirst(Map<String, Set<Integer>> map) {
return map.isEmpty() ? Collections.emptySet() :
new HashSet<>(map.values().stream().findFirst().get());
}
主要
public static void main(String[] args) {
Map<String, Set<Integer>> map =
Map.of("w1", Set.of(1,3,4,6,7),
"w2", Set.of(2,3,4,5,7),
"w3", Set.of(1,2,3,5,7));
System.out.println(getIntersectionStream(map));
System.out.println(getIntersectionLoop(map));
}
输出
[3, 7]
[3, 7]
通用且可能有效的解决方案可能是:
public static <T> Set<T> retain(Map<?, Set<T>> map) {
Iterator<Set<T>> it = map.values().iterator();
if (!it.hasNext()) {
return new HashSet<>();
}
Set<T> result = new HashSet<>(it.next());
while (it.hasNext() && !result.isEmpty()) {
result.retainAll(it.next());
}
return result;
}
注意
!result.isEmpty()
,这是提前退出条件。 IE。如果结果为空,则这些集合没有共同元素。
如果你确实想使用流,并且可以保证
Set
是可变的,那么你也可以使用reduce()
终端操作:
public static <T> Set<T> retain(Map<?, Set<T>> map) {
return map.values().stream()
.reduce((a, b) -> {
a.retainAll(b);
return a;
})
.orElse(Set.of());
}
但请注意,这会修改并返回地图中的第一个集合。
这个简单的版本使用带有集合操作的流来通过查找其中一个成员来填充
intersections
,并 retainAll
来匹配所有其他成员:
Set<Integer> intersection = new HashSet<>();
map.values().stream().limit(1).forEach(intersection::addAll);
map.values().stream().forEach(intersection::retainAll);
我首先建议不要使用流,只需如下所示简单地执行即可。这确实删除了地图的一个元素。 你可以只做
map.get("w1")
并做一个多余的 retainAll
。
请注意,由于您使用
Map.of
创建了一个不可变集,我必须制作一个副本以允许修改 result
。
Set<Integer> result = new HashSet<>(map.remove("w1"));
for (Set<Integer> set : map.values()) {
result.retainAll(set);
}
这是一个流解决方案。
为了避免使用初始化程序进行归约操作,我将使用
reducing Collector
。该收集器返回一个 Optional
,因此必须使用 orElse
来检索集合并允许空地图。
Set<Integer> result = map.values().stream()
.collect(Collectors.reducing((a, b) -> {
a.retainAll(b);
return a;
}))..orElse(new HashSet<>());
System.out.println(result);
以上都会打印
[3, 7]
显示的解决方案假定已正确填充
Map
。 完整的解决方案将包括检查
在我看来,这是最好的解决方案:
map.values().stream().reduce((s1, s2) -> {
Set<Integer> s3 = new HashSet<>(s2);
s3.retainAll(s1);
return s3;
}).orElse(new HashSet<>());
它是通过集合相交而减少的地图值流。
Stream.reduce
来解决:
Set<Integer> commonElements = map.values().stream()
.reduce((s1, s2) -> s1.stream()
.filter(s2::contains)
.collect(Collectors.toUnmodifiableSet()))
.orElseThrow(NoSuchElementException::new);
归约的每一步都会创建一个新的
Set
,其中仅包含两个集合之间的共同元素。
我在这里没有做的一个简单的优化(为了简单起见)是有条件地交换
s1
和 s2
,以便较小的集合被流式传输,因为这是基于大小的线性时间发生的套装中的。