Вопрос-ответ

Remove elements from collection while iterating

Удалять элементы из коллекции во время итерации

AFAIK, есть два подхода:


  1. Выполнение итерации по копии коллекции

  2. Используйте итератор фактической коллекции

Например,

List<Foo> fooListCopy = new ArrayList<Foo>(fooList);
for(Foo foo : fooListCopy){
// modify actual fooList
}

и

Iterator<Foo> itr = fooList.iterator();
while(itr.hasNext()){
// modify actual fooList using itr.remove()
}

Есть ли какие-либо причины предпочесть один подход другому (например, предпочесть первый подход по простой причине удобочитаемости)?

Переведено автоматически
Ответ 1

Позвольте мне привести несколько примеров с некоторыми альтернативами, чтобы избежать ConcurrentModificationException.

Предположим, у нас есть следующая коллекция книг

List<Book> books = new ArrayList<Book>();
books.add(new Book(new ISBN("0-201-63361-2")));
books.add(new Book(new ISBN("0-201-63361-3")));
books.add(new Book(new ISBN("0-201-63361-4")));

Собирать и удалять

Первый метод заключается в сборе всех объектов, которые мы хотим удалить (например, используя расширенный цикл for), и после завершения итерации мы удаляем все найденные объекты.

ISBN isbn = new ISBN("0-201-63361-2");
List<Book> found = new ArrayList<Book>();
for(Book book : books){
if(book.getIsbn().equals(isbn)){
found.add(book);
}
}
books.removeAll(found);

Предполагается, что операция, которую вы хотите выполнить, - "удалить".

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

Использование ListIterator

Если вы работаете со списками, другой метод заключается в использовании a ListIterator который поддерживает удаление и добавление элементов во время самой итерации.

ListIterator<Book> iter = books.listIterator();
while(iter.hasNext()){
if(iter.next().getIsbn().equals(isbn)){
iter.remove();
}
}

Опять же, я использовал метод "remove" в приведенном выше примере, который, по-видимому, подразумевал ваш вопрос, но вы также можете использовать его add метод для добавления новых элементов во время итерации.

Использование JDK >= 8

Для тех, кто работает с Java 8 или улучшенными версиями, есть пара других методов, которые вы могли бы использовать, чтобы воспользоваться этим преимуществом.

Вы могли бы использовать новый removeIf метод в Collection базовом классе:

ISBN other = new ISBN("0-201-63361-2");
books.removeIf(b -> b.getIsbn().equals(other));

Или используйте новый stream API:

ISBN other = new ISBN("0-201-63361-2");
List<Book> filtered = books.stream()
.filter(b -> b.getIsbn().equals(other))
.collect(Collectors.toList());

В этом последнем случае, чтобы отфильтровать элементы из коллекции, вы переназначаете исходную ссылку на отфильтрованную коллекцию (т.е. books = filtered) или используете отфильтрованную коллекцию для removeAll найденных элементов из исходной коллекции (т.Е. books.removeAll(filtered)).

Использовать подсписок или подмножество

Существуют и другие альтернативы. Если список отсортирован, и вы хотите удалить последовательные элементы, вы можете создать подсписк, а затем очистить его.:

books.subList(0,5).clear();

Поскольку подсписк поддерживается исходным списком, это было бы эффективным способом удаления этой подколлекции элементов.

Чего-то подобного можно было бы достичь с помощью отсортированных наборов, используя NavigableSet.subSet метод или любой из предлагаемых там методов нарезки.

Рекомендации:

Какой метод вы используете, может зависеть от того, что вы собираетесь делать


  • Метод сбора и removeAl работает с любой коллекцией (Collection, List, Set и т.д.).

  • Этот ListIterator метод, очевидно, работает только со списками, при условии, что их данная ListIterator реализация предлагает поддержку операций добавления и удаления.

  • Iterator Подход будет работать с любым типом коллекции, но он поддерживает только операции удаления.

  • При использовании подхода ListIterator/Iterator очевидным преимуществом является отсутствие необходимости что-либо копировать, поскольку мы удаляем по мере итерации. Таким образом, это очень эффективно.

  • В примере потоков JDK 8 на самом деле ничего не удалялось, но выполнялся поиск нужных элементов, а затем мы заменили исходную ссылку на коллекцию новой, а старую оставили для сбора мусора. Итак, мы выполняем итерацию только один раз по коллекции, и это было бы эффективно.

  • Недостатком подхода collect and removeAll является то, что нам приходится выполнять итерацию дважды. Сначала мы выполняем итерацию в цикле foor в поисках объекта, соответствующего нашим критериям удаления, и как только мы его находим, мы просим удалить его из исходной коллекции, что подразумевает повторную итерационную работу по поиску этого элемента, чтобы удалить его.

  • Я думаю, стоит упомянуть, что метод remove Iterator интерфейса помечен как "необязательный" в Javadocs, что означает, что могут быть Iterator реализации, которые выдают ошибкуUnsupportedOperationException, если мы вызываем метод remove. Таким образом, я бы сказал, что этот подход менее безопасен, чем другие, если мы не можем гарантировать поддержку итератора для удаления элементов.

Ответ 2

Старое любимое (оно все еще работает):

List<String> list;

for(int i = list.size() - 1; i >= 0; --i)
{
if(list.get(i).contains("bad"))
{
list.remove(i);
}
}

Преимущества:


  1. Он выполняет итерацию по списку только один раз

  2. Не создаются дополнительные объекты или другие ненужные сложности

  3. Никаких проблем с попыткой использовать индекс удаленного элемента, потому что... ну, подумайте об этом!

Ответ 3

В Java 8 есть другой подход. Collection#removeIf

например:

List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);

list.removeIf(i -> i > 2);
Ответ 4

Есть ли какие-либо причины предпочесть один подход другому


Первый подход будет работать, но имеет очевидные накладные расходы на копирование списка.

Второй подход не сработает, потому что многие контейнеры не допускают модификации во время итерации. Это включает в себя ArrayList.

Если единственной модификацией является удаление текущего элемента, вы можете реализовать второй подход, используя itr.remove() (то есть используйте методитератораremove(), а не контейнер). Это был бы мой предпочтительный метод для итераторов, которые поддерживают remove().

java collections