Как удалить несколько элементов из набора / карты и знать, какие из них были удалены? - PullRequest
29 голосов
/ 13 июня 2019

У меня есть метод, который должен удалить любой элемент, указанный в (маленьком) Set<K> keysToRemove, из некоторого (потенциально большого) Map<K,V> from.Но 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());

Я считаю, что немного более кратким, но я такжеСчитаете, что лямбда слишком неуклюжая.

Есть какие-нибудь предложения, как добиться того же результата менее неуклюжими способами?

Ответы [ 6 ]

23 голосов
/ 13 июня 2019

«Код старой школы» должен быть скорее

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().removeAll(result); на from.keySet().removeIf(result::contains), что довольно дорого, поскольку оно повторяется по большей картеили с result.forEach(from.keySet()::remove), который не имеет этого недостатка, но, тем не менее, не более читабелен, чем removeAll.

В целом, «код старой школы» намного лучше, чем этот..

13 голосов
/ 13 июня 2019

Более краткое решение, но все еще с нежелательным побочным эффектом в вызове filter:

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

Set.remove уже возвращает true, еслиset содержал указанный элемент.

PS В конце я бы, вероятно, придерживался "старого школьного кода".

5 голосов
/ 13 июня 2019

Я бы не использовал 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;
}
4 голосов
/ 13 июня 2019

Вы можете использовать это:

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

Это похоже на ответ Oleksandr , но без побочного эффекта. Но я бы придерживался этого ответа, если вы ищете производительность.

В качестве альтернативы вы можете использовать Stream.peek() для удаления, но будьте осторожны с другими побочными эффектами (см. Комментарии). Поэтому я бы не рекомендовал это.

Set<K> removedKeys = keysToRemove.stream()
        .filter(from::containsKey)
        .peek(from::remove)
        .collect(Collectors.toSet());
4 голосов
/ 13 июня 2019

Вы можете использовать поток и удалить все

Set<K> fromKeys = from.keySet();
Set<K> removedKeys = keysToRemove.stream()
    .filter(fromKeys::contains)
    .collect(Collectors.toSet());
fromKeys.removeAll(removedKeys);
return removedKeys;
3 голосов
/ 13 июня 2019

Чтобы добавить еще один вариант к подходам, можно также разбить ключи и вернуть требуемое 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);
}
...