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

Как удалить несколько элементов из набора/карты и зная, какие из них были удалены?

У меня есть метод, который должен удалить любой элемент, перечисленный в (маленьком) 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());

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

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

4b9b3361

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

В общем, "код старой школы" намного лучше, чем этот.

Ответ 2

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

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

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

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

Ответ 3

Я бы не использовал потоки для этого. Я бы воспользовался 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

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

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

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

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

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

Ответ 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);
}

Ответ 7

Вы можете просто вернуть новую карту без элементов, которые вы хотите удалить.

from.entrySet().stream()
    .filter(entry -> !keysToRemove.contains(entry.getKey()))
    .collect(Collectors.toMap(
               entry -> entry.getKey(), 
               entry -> entry.getValue()));