如何从 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 生成的命名元组专门删除一个元素并从剩余元素中生成一个字典?