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

AFAIK существует два подхода:

  • Итерации над копией коллекции
  • Использовать итератор фактической коллекции

Например,

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

Сбор и удаление

Соберите все записи, которые вы хотите удалить, в цикле расширенного цикла, и после завершения итерации вы удалите все найденные записи.

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

Или вы можете использовать ListIterator, который поддерживает метод remove/add во время самой итерации.

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

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

Использовать потоки JDK 8

Или используя JDK 8 потоков, lambdas/closures:

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 или любого из предложенных там методов разрезания.

Вопросы:

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

  • Метод сбора и удаления работает с любой коллекцией (Collection, List, Set и т.д.).
  • Метод ListIterator работает только со списками при условии, что их реализация ListIterator предлагает поддержку для операций добавления и удаления.
  • Подход Iterator вообще будет работать над любой коллекцией, если вы только собираетесь использовать метод удаления итератора.
  • В подходе ListIterator/Iterator очевидное преимущество - не копировать ничего.
  • Примеры сторонних и JDK-8 потоков фактически ничего не удаляют, но ищут нужные элементы, тогда вы можете заменить исходную ссылку на новую, и пусть старый будет собран мусором.
  • В процессе сбора и удаления недостатком является то, что мы должны повторять итерацию дважды. Мы перебираем петлю в поисках элемента, и как только мы его найдем, мы попросим удалить его из исходного списка, что означало бы вторую итерационную работу для поиска данного элемента.
  • Я думаю, что стоит упомянуть, что метод remove интерфейса Iterator помечается как необязательный в Javadocs, что означает, что могут существовать реализации Iterator, которые могут бросать UnsupportedOperationException. Таким образом, я бы сказал, что этот подход менее безопасен, чем первый.

Ответ 2

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

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

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

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

Ответ 3

Будет работать только второй подход. Вы можете изменять коллекцию во время итерации только с помощью iterator.remove(). Все остальные попытки вызовут ConcurrentModificationException.

Ответ 4

В Java 8 существует другой подход. Коллекция # removeIf

например:

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

list.removeIf(i -> i > 2);

Ответ 5

Я бы выбрал второй, так как вам не нужно делать копию памяти, и Iterator работает быстрее. Таким образом, вы сохраняете память и время.

Ответ 6

Вы не можете сделать второе, потому что даже если вы используете метод remove() на Iterator, вы получите исключение.

Лично я предпочел бы первый для всех экземпляров Collection, несмотря на дополнительную подделку создания нового Collection, я считаю, что он менее подвержен ошибкам во время редактирования другими разработчиками. В некоторых реализациях Collection поддерживается Iterator remove(), а на другом - нет. Вы можете прочитать больше в документах для Iterator.

Третий вариант: создать новый Collection, перебрать исходный текст и добавить все элементы первого Collection ко второму Collection, которые не могут быть удалены. В зависимости от размера Collection и количества удалений это может значительно экономить на памяти по сравнению с первым подходом.

Ответ 7

почему бы не это?

for( int i = 0; i < Foo.size(); i++ )
{
   if( Foo.get(i).equals( some test ) )
   {
      Foo.remove(i);
   }
}

И если это карта, а не список, вы можете использовать keyset()