使用流从 Map 中查找其值为 Java 中集合的公共元素集

Finding common set of elements from a Map whose value is a collection in Java using streams

假设我有一个 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());

这是一个简洁的解决方案,但它会检查第一组两次。您还可以添加检查以查看地图是否包含任何条目。

我的方法是首先将不同的值分组并计算它们。然后只保留那些计数等于地图中条目数的。这是有效的,因为每个集合只能包含一次值。

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

通用且可能有效的解决方案可能是:

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() 这是一个 early-exit 条件。 IE。如果结果为空,则集合没有共同元素。

注意:这是受 of MikeFHay 的启发。


如果你真的想使用流,并且可以保证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());
}

但请注意,这会修改 returns 地图中的第一个集合。

这可以通过使用 collect() 操作来完成。

这种方法背后的逻辑是从值[=64=的集合中得到一个随机集 ],用新集合包起来避免突变,然后用retainlAll()方法将所有集合一一组合起来。

即使值由 不可变集 表示(如问题中的示例),这也不是问题,因为它们将 完整保存。唯一会突变的集合是新集合,由供应商函数在collect().

如果地图,可能会出现问题

在这种情况下,任何从 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 的集合中检索一个 随机集 。在空映射的情况下,它将 return 一个空的不可修改集,否则,它将产生第一个由流 return 编辑的集。

注意,在这种情况下,在可选对象上调用 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]

这个简单版本使用带集合操作的流来填充 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);
}

这里是流解决方案。

为了避免必须使用初始化程序来进行 reduce 操作,我会使用 reducing Collector。此收集器 returns 和 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<>());

它是通过集合相交减少的地图值流。