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

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

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

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

Как насчет того, чтобы просто собрать все ключи, которые можно удалить, а затем вызвать removeAll() для этого отфильтрованного набора? Или как насчет «фильтрации» на fromKeys::remove?

Thomas 13.06.2019 16:28

Я считаю и делаю вывод из ответов здесь, улучшение, которое происходит в основном из любых изменений, заключается в использовании if (fromKeys.remove(keyToRemove)) { removedKeys.add(keyToRemove); } вместо использования как содержимого, так и удаления в if (fromKeys.contains(keyToRemove)) { fromKeys.remove(keyToRemove); removedKeys.add(keyToRemove); }

Naman 13.06.2019 18:51
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
30
2
2 885
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

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

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

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

 P.S. В конце концов, я бы, вероятно, придерживался «кода старой школы».

Точно моя мысль ;) - Это просто кажется немного хакерским, потому что мы «фильтруем» метод, который на самом деле представляет собой побочный эффект.

Thomas 13.06.2019 16:32

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

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

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

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

Никогда не используйте peek ни для чего, кроме отладки! См. stackoverflow.com/questions/47356992/… и связанные с ним вопросы.

Michael A. Schaffrath 13.06.2019 17:02

@MichaelA.Schaffrath Имеет смысл. Забавный факт: я снова попробовал свою первоначальную лямбду с картой. IntelliJ предлагает заменить этот вызов map() на peek() ;-)

GhostCat 13.06.2019 17:26

Я бы не стал использовать потоки для этого. Я бы воспользовался сохранить все:

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 13.06.2019 17:43

@Holger Я понимаю вашу точку зрения, но Набор просто копирует ссылки, что мне кажется безобидным, если только размер Карты действительно не огромен. Вы правы насчет removeAll(matchingKeys). Обновлено.

VGR 13.06.2019 17:49

Это не просто копирование ссылок, а хеширование. И поскольку в OP указаны ожидаемые размеры, а поменять их местами тривиально, я бы сделал это. На самом деле я сделал.

Holger 13.06.2019 17:52
Ответ принят как подходящий

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

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), у которого нет этого недостатка, но все же t более читабелен, чем removeAll.

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

Работал с существующим кодом. Разве public Set<K> removeEntries(Map<K, ?> from) { Set<K> removedKeys = new HashSet<>(); for (K keyToRemove : keysToRemove) { if (from.keySet().remove(keyToRemove)) { removedKeys.add(keyToRemove); } } return removedKeys; } не будет лучшим старым школьным кодом, чем retainAll, removeAll решение. Я хотел сосредоточиться (придираться) к одной итерации, а не к двум итерациям с их реализацией. (Может быть, я просто ошибаюсь, делая такой вывод.)

Naman 13.06.2019 18:47

@Naman это то, что я опубликовал как второй вариант, как раз на тот случай, когда итерация имеет значение. Однако комбинация retainAll/removeAll будет перебирать набор, который был указан OP как довольно маленький.

Holger 13.06.2019 19:38

О да, второй вариант действительно такой же. Но не будет ли removeAll повторять fromKeys вместо keysToRemove? Что касается первого варианта потока, как насчет использования предиката для сокращения с помощью partitioningBy, написал образец кода?

Naman 13.06.2019 19:44

@Naman касается деталей реализации, но я предполагаю, что он работает как AbstractSet.removeAll(…), если даже не наследует правильно этот метод: «Эта реализация определяет, что меньше из этого набора и указанной коллекции, вызывая метод size для каждого из них. …[так далее]». Использование предиката с состоянием для partitioningBy так же не рекомендуется, как и для filter, но с последним вы собираете еще один набор действительно нежелательных элементов…

Holger 13.06.2019 19:50

Немного не по теме, но как бы вы проверили правильность вашего решения? Вы действительно пишете все с нуля в IntelliJ (public static void main и все) и проверяете? Спрашиваю, потому что я не знаком с потоками или тегом java.

cs95 14.06.2019 06:11

@ cs95 ну да, для большинства ответов SO я пишу тестовый код либо с нуля, либо используя код вопроса в качестве отправной точки, если таковой имеется. В зависимости от контекста это может быть Netbeans, Eclipse или командная строка. Когда дело доходит до поведения, зависящего от компилятора, у меня также есть пакетные файлы для компиляции и запуска одного и того же исходного кода с разными JDK.

Holger 14.06.2019 09:08

@ Наман, мое последнее предложение было написано в спешке. Я хотел сказать, что partitioningBy работает больше, когда нужно, когда нужен только один из двух наборов. Кроме того, это похоже на подход filter.

Holger 14.06.2019 09:17

@Holger Я все равно понял, что вы имели в виду под другим набором. Спасибо ?

Naman 14.06.2019 09:21

Если вы уже создаете минимальный воспроизводимый пример для своих тестов, почему бы не опубликовать его, чтобы другие могли быстрее его проверить? По крайней мере, я как правило пытаюсь это сделать вместе с некоторыми "фиктивными/тестовыми" данными и системными данными, показывающими результаты...

Marco13 14.06.2019 14:57

@ Marco13 Marco13 Я часто так делаю, особенно для вопросов, содержащих пример, но не каждый ответ будет полезен с примерами. Кроме того, не весь мой тестовый код является минимальным примером. А иногда он редактируется для других тестов, без одновременного включения всех тестов в код, поэтому перед публикацией требуется значительная очистка.

Holger 14.06.2019 15:44

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

Также оставляет возможность использовать ключи, которые не были частью набора ключей карты. (на всякий случай)

Naman 13.06.2019 19:04

Другие вопросы по теме