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

215

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

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

user1329572
источник
1
Просто любопытно, почему вы создаете копию foolist, а не просто просматриваете foolist в первом примере?
Хаз
@ Haz, так что я должен пройти только один раз.
user1329572
15
Примечание: предпочитайте 'for' вместо 'while' также с итераторами, чтобы ограничить область действия переменной: for (Iterator <Foo> itr = fooList.iterator (); itr.hasNext ();) {}
Puce
Я не знал, что whileу меня были другие правила определения for
Александр Миллс
В более сложной ситуации у вас может быть случай, когда fooListесть переменная экземпляра, и вы вызываете метод во время цикла, который в итоге вызывает другой метод в том же классе, что и fooList.remove(obj). Видели это случилось. В этом случае копирование списка является наиболее безопасным.
Дэйв Гриффитс

Ответы:

420

Позвольте мне привести несколько примеров с некоторыми альтернативами, чтобы избежать 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

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

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

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

Используя JDK> = 8

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

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

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

Или используйте новый потоковый 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техника работают с любой коллекцией (Коллекция, Список, Набор и т. Д.).
  • ListIteratorМетод работает , очевидно , только со списками, при условии , что их данные ListIteratorпредложения по реализации поддержки для добавления и удаления операций.
  • IteratorПодход будет работать с любым типом коллекции, но он поддерживает только операцию УДАЛИТЬ.
  • При использовании ListIterator/ Iteratorподхода очевидным преимуществом является отсутствие необходимости что-либо копировать, поскольку мы удаляем по мере итерации. Итак, это очень эффективно.
  • Пример потоков JDK 8 на самом деле ничего не удалял, но искал нужные элементы, а затем мы заменили исходную ссылку на коллекцию новой и позволили старой собирать мусор. Итак, мы перебираем коллекцию только один раз, и это будет эффективно.
  • В Собирать и removeAllподойти недостаток в том , что мы должны итерация дважды. Сначала мы выполняем итерацию в цикле foor, ища объект, который соответствует нашим критериям удаления, и как только мы его найдем, мы просим удалить его из исходной коллекции, что подразумевает вторую итерационную работу для поиска этого элемента, чтобы убери это.
  • Я думаю, что стоит упомянуть, что метод удаления Iteratorинтерфейса помечен как «необязательный» в Javadocs, что означает, что могут быть Iteratorреализации, которые выдают, UnsupportedOperationExceptionесли мы вызываем метод удаления. Таким образом, я бы сказал, что этот подход менее безопасен, чем другие, если мы не можем гарантировать поддержку итератора для удаления элементов.
Эдвин Далорсо
источник
Браво! это полное руководство.
Magno C
Это идеальный ответ! Спасибо.
Вильгельм
7
В вашем абзаце о потоках JDK8 вы упоминаете removeAll(filtered). removeIf(b -> b.getIsbn().equals(other))
Сокращение
В чем разница между Iterator и ListIterator?
Александр Миллс
Не рассматривал удаление, но это был ответ на мои молитвы. Спасибо!
Акабель
15

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

например:

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

list.removeIf(i -> i > 2);
Santhosh
источник
13

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

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

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

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

NPE
источник
Ой, извините ... подразумевается, что я бы использовал метод удаления итератора, а не контейнера. И сколько накладных расходов создает копирование списка? Это не может быть много, и, поскольку оно относится к методу, его следует собирать довольно быстро. Смотрите правку ..
user1329572
1
@aix Я думаю, стоит упомянуть, что метод удаления Iteratorинтерфейса помечен как необязательный в Javadocs, что означает, что могут быть реализации Iterator, которые могут выдавать UnsupportedOperationException. Таким образом, я бы сказал, что этот подход менее безопасен, чем первый. В зависимости от реализаций, предназначенных для использования, первый подход может быть более подходящим.
Эдвин Далорсо
@EdwinDalorzo remove()на саму оригинальную коллекцию также можно добавитьUnsupportedOperationException : docs.oracle.com/javase/7/docs/api/java/util/… . К сожалению, интерфейсы контейнеров Java определены как крайне ненадежные (если честно, победить точку интерфейса). Если вы не знаете точную реализацию, которая будет использоваться во время выполнения, лучше сделать что-то неизменным - например, используйте Java 8+ Streams API, чтобы отфильтровать элементы и собрать их в новый контейнер, затем полностью заменить старый на него.
Мэтью Читал
5

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

AlexR
источник
2
Первая попытка повторяется на копии, то есть он может изменить оригинал.
Колин Д.
3

Фаворит Старого Таймера (он все еще работает):

List<String> list;

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

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

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

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

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

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

Джон
источник
0

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

Калин Андрей
источник
« Итератор работает быстрее ». Что-нибудь, чтобы поддержать это требование? Кроме того, объем памяти при создании копии списка очень тривиален, тем более что он будет определен в рамках метода и почти сразу же соберет мусор.
user1329572
1
В первом подходе недостаток состоит в том, что мы должны повторять дважды. Мы выполняем итерацию в цикле foor в поисках элемента, и как только мы его находим, мы просим удалить его из исходного списка, что подразумевает вторую итерационную работу для поиска данного элемента. Это подтвердило бы утверждение, что, по крайней мере, в этом случае итерационный подход должен быть быстрее. Мы должны учитывать, что создается только структурное пространство коллекции, объекты внутри коллекций не копируются. Обе коллекции будут хранить ссылки на одни и те же объекты. Когда происходит GC, мы не можем сказать !!!
Эдвин Далорсо
-2

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

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

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

Дрейк Кларрис
источник
4
Этот подход имеет много основных недостатков. Во-первых, каждый раз, когда вы удаляете элемент, индексы реорганизуются. Поэтому, если вы удалите элемент 0, то элемент 1 станет новым элементом 0. Если вы собираетесь к этому, по крайней мере, сделайте это в обратном направлении, чтобы избежать этой проблемы. Во-вторых, не все реализации List предоставляют прямой доступ к элементам (как это делает ArrayList). В LinkedList это было бы чрезвычайно неэффективно, потому что каждый раз, когда вы выдаете команду, get(i)вы должны посещать все узлы, пока не достигнете i.
Эдвин Далорсо
Никогда не рассматривал это, поскольку я обычно использовал это, чтобы удалить единственный предмет, который я искал. Хорошо знать.
Дрейк Кларрис
4
Я опаздываю на вечеринку, но наверняка в блок-код if после Foo.remove(i);вас следует сделать i--;?
Берти Уин
потому что это прослушивается
Джек