How to remove multiple elements from Set/Map AND knowing which ones were removed?

故事扮演 提交于 2020-12-02 03:32:43

问题


I have a method that has to remove any element listed in a (small) Set<K> keysToRemove from some (potentially large) Map<K,V> from. But removeAll() doesn't do, as I need to return all keys that were actually removed, since the map might or might not contain keys that require removal.

Old-school code is straight forward:

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

The same, written using streams:

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

I find that a bit more concise, but I also find that lambda too clunky.

Any suggestions how to achieve the same result in less clumsy ways?


回答1:


The “old-school code” should rather be

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

Since you said that keysToRemove is rather small, the copying overhead likely doesn’t matter. Otherwise, use the loop, but don’t do the hash lookup twice:

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

You can express the same logic as a stream as

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

but since this is a stateful filter, it is highly discouraged. A cleaner variant would be

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

and if you want to maximize the “streamy” usage, you can replace from.keySet().removeAll(result); with from.keySet().removeIf(result::contains), which is quiet expensive, as it is iterating over the larger map, or with result.forEach(from.keySet()::remove), which doesn’t have that disadvantage, but still, isn’t more readable than removeAll.

All in all, the “old-school code” is much better than that.




回答2:


More concise solution, but still with unwanted side effect in the filter call:

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

Set.remove already returns true if the set contained the specified element.

 P.S. In the end, I would probably stick with the "old-school code".




回答3:


I wouldn’t use Streams for this. I would take advantage of 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;
}



回答4:


You can use the stream and the removeAll

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



回答5:


You can use this:

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

It's similar to Oleksandr's answer, but avoiding the side effect. But I would stick with that answer, if you are looking for performance.

Alternatively you could use Stream.peek() for the remove, but be careful with other side effects (see the comments). So I would not recommend that.

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



回答6:


To add another variant to the approaches, one could also partition the keys and return the required Set as:

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


来源:https://stackoverflow.com/questions/56582681/how-to-remove-multiple-elements-from-set-map-and-knowing-which-ones-were-removed

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!