如何从 Set/Map 中删除多个元素并知道哪些元素已被删除?

Posted

技术标签:

【中文标题】如何从 Set/Map 中删除多个元素并知道哪些元素已被删除?【英文标题】:How to remove multiple elements from Set/Map AND knowing which ones were removed? 【发布时间】:2019-10-28 04:14:41 【问题描述】:

我有一个方法必须从一些(可能很大)Map<K,V> from 中删除(小)Set<K> keysToRemove 中列出的任何元素。但是removeAll() 不行,因为我需要返回所有实际删除的键,因为地图可能包含也可能不包含需要删除的键。

老派代码直截了当:

public Set<K> removeEntries(Map<K, V> from) 
    Set<K> fromKeys = from.keySet();
    Set<K> removedKeys = new HashSet<>();
    for (K keyToRemove : keysToRemove) 
        if (fromKeys.contains(keyToRemove)) 
            fromKeys.remove(keyToRemove);
            removedKeys.add(keyToRemove);
        
    
    return removedKeys;

同样,使用流编写:

Set<K> fromKeys = from.keySet();
return keysToRemove.stream()
        .filter(fromKeys::contains)
        .map(k -> 
            fromKeys.remove(k);
            return k;
        )
        .collect(Collectors.toSet());

我觉得这更简洁一些,但我也觉得 lambda 太笨重了。

有什么建议可以以不那么笨拙的方式实现相同的结果吗?

【问题讨论】:

如何收集所有可以删除的密钥,然后在该过滤集上调用removeAll()?或者在fromKeys::remove上“过滤”怎么样? 我相信并从这里的答案推断,主要来自任何更改的改进是使用 if (fromKeys.remove(keyToRemove)) removedKeys.add(keyToRemove); 而不是在 if (fromKeys.contains(keyToRemove)) fromKeys.remove(keyToRemove); removedKeys.add(keyToRemove); 中同时使用包含和删除 【参考方案1】:

“老派代码”应该是

public Set<K> removeEntries(Map<K, ?> from) 
    Set<K> fromKeys = from.keySet(), removedKeys = new HashSet<>(keysToRemove);
    removedKeys.retainAll(fromKeys);
    fromKeys.removeAll(removedKeys);
    return removedKeys;

既然你说keysToRemove 相当小,那么复制开销可能并不重要。否则,使用循环,但不要进行两次哈希查找:

public Set<K> removeEntries(Map<K, ?> from) 
    Set<K> fromKeys = from.keySet();
    Set<K> removedKeys = new HashSet<>();
    for(K keyToRemove : keysToRemove)
        if(fromKeys.remove(keyToRemove)) removedKeys.add(keyToRemove);
    return removedKeys;

您可以将相同的逻辑表达为流

public Set<K> removeEntries(Map<K, ?> from) 
    return keysToRemove.stream()
        .filter(from.keySet()::remove)
        .collect(Collectors.toSet());

但由于这是一个有状态的过滤器,因此强烈建议不要这样做。一个更干净的变体是

public Set<K> removeEntries(Map<K, ?> from) 
    Set<K> result = keysToRemove.stream()
        .filter(from.keySet()::contains)
        .collect(Collectors.toSet());
    from.keySet().removeAll(result);
    return result;

如果你想最大化“流式”的使用,你可以用from.keySet().removeIf(result::contains)替换from.keySet().removeAll(result);,因为它正在迭代更大的地图,或者result.forEach(from.keySet()::remove),它不会有这个缺点,但仍然没有比removeAll 更具可读性。

总而言之,“老派代码”要好得多。

【讨论】:

@Naman 这就是我发布的第二个变体,适用于迭代很重要的情况。但是,retainAll/removeAll 组合将遍历 OP 指定的相当小的集合。 @Naman 正在触及实现细节,但我认为它的工作方式类似于AbstractSet.removeAll(…),即使没有继承该方法的权利:“这个实现确定哪个是这个集合中的较小者和指定集合,通过在每个集合上调用 size 方法。 …[等]”。对partitioningBy 使用有状态谓词与filter 一样不鼓励,但使用后者,您将收集另一组实际上不需要的元素…… @cs95 好吧,是的,对于大多数 SO 答案,我编写了一些测试代码,要么从头开始,要么使用问题的代码作为起点,如果有的话。根据上下文,它可能在 Netbeans、Eclipse 或命令行中。当涉及到与编译器相关的行为时,我也有批处理文件来使用不同的 JDK 编译和运行相同的源代码。 @Naman 我的最后一句话写得很匆忙。我想说的是,partitioningBy 在需要时做更多的工作,而只需要两组中的一组。除此之外,它就像filter 方法。 @Marco13 我经常这样做,尤其是对于包含示例的问题,但并非每个答案都会从示例中受益。此外,并不是我所有的测试代码都是一个最小的例子。有时,它会针对其他测试进行编辑,而不是一次在代码中进行所有测试,因此在发布之前需要进行大量清理。【参考方案2】:

更简洁的解决方案,但在filter 调用中仍然存在不需要的副作用

Set<K> removedKeys =
    keysToRemove.stream()
                .filter(fromKeys::remove)
                .collect(Collectors.toSet());

如果set 包含指定的元素,Set.remove 已经返回true

最后,我可能会坚持使用“老式代码”。

【讨论】:

正是我的想法 ;) - 只是感觉有点 hacky,因为我们正在“过滤”一个实际上代表副作用的方法。【参考方案3】:

我不会为此使用 Streams。我会利用retainAll:

public Set<K> removeEntries(Map<K, V> from) 
    Set<K> matchingKeys = new HashSet<>(from.keySet());
    matchingKeys.retainAll(keysToRemove);

    from.keySet().removeAll(matchingKeys);

    return matchingKeys;

【讨论】:

这指向正确的方向,但是您正在复制“可能大”from 映射的键集,而您可以复制“小”keysToRemove,因为 a 和 b 的交集是与 b 和 a 相同。此外,matchingKeys 可能小于keysToRemove,因此removeAll(matchingKeys) 更可取。 @Holger 我明白你的意思,但 Set 只是复制引用,这对我来说似乎是良性的,除非地图的大小真的很大。不过,您对 removeAll(matchingKeys) 是正确的。已更新。 这不仅仅是复制引用,而是散列。而且由于 OP 说明了预期的大小并且交换两者是微不足道的,我会这样做。其实I did.【参考方案4】:

你可以使用流和removeAll

Set<K> fromKeys = from.keySet();
Set<K> removedKeys = keysToRemove.stream()
    .filter(fromKeys::contains)
    .collect(Collectors.toSet());
fromKeys.removeAll(removedKeys);
return removedKeys;

【讨论】:

【参考方案5】:

你可以用这个:

Set<K> removedKeys = keysToRemove.stream()
        .filter(from::containsKey)
        .collect(Collectors.toSet());
removedKeys.forEach(from::remove);

这类似于 Oleksandr 的回答,但避免了副作用。但是,如果您正在寻找性能,我会坚持这个答案。

或者,您可以使用Stream.peek() 进行删除,但要小心其他副作用(请参阅 cmets)。所以我不建议这样做。

Set<K> removedKeys = keysToRemove.stream()
        .filter(from::containsKey)
        .peek(from::remove)
        .collect(Collectors.toSet());

【讨论】:

除调试外,切勿使用 peek 进行任何操作!请参阅***.com/questions/47356992/… 以及其中链接的问题 @MichaelA.Schaffrath 非常有道理。有趣的事实:我用 map 再次尝试了我的初始 lambda。 IntelliJ 建议将对 map() 的调用替换为 peek() ;-) 或更一般的一般:In Java streams is peek really only for debugging?【参考方案6】:

要向方法添加另一种变体,还可以对键进行分区并将所需的Set 返回为:

public Set<K> removeEntries(Map<K, ?> from) 
    Map<Boolean, Set<K>> partitioned = keysToRemove.stream()
            .collect(Collectors.partitioningBy(k -> from.keySet().remove(k),
                    Collectors.toSet()));
    return partitioned.get(Boolean.TRUE);

【讨论】:

还可以选择使用不属于地图键集的键。 (以防万一)

以上是关于如何从 Set/Map 中删除多个元素并知道哪些元素已被删除?的主要内容,如果未能解决你的问题,请参考以下文章

如何操作通过 itertuples 生成的命名元组专门删除一个元素并从剩余元素中生成一个字典?

Dagger 2从浅到深

js数组 字符串 Set Map的操作

从 std::vector 中删除多个对象?

Swift flatMap:如何从数组中仅删除元组中特定元素为零的元组?

C++ 从 void* 中恢复指向未知关联容器的元素