Видаліть елементи з колекції під час ітерації


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

Чи є причини віддати перевагу одному підходу над іншим (наприклад, віддати перевагу першому підходу з простої причини читабельності)?


1
Цікаво, чому ви створюєте копію дурника, а не просто перебираєте дурман у першому прикладі?
Хаз

@Haz, Тому мені потрібно лише один раз зациклюватися.
user1329572

15
Примітка: віддайте перевагу "для" над "в той час як" також з ітераторами, щоб обмежити область змінної: для (Iterator <Foo> itr = fooList.iterator (); itr.hasNext ();) {}
Puce

Я не знав, чи whileбули інші правила розміщення, ніжfor
Олександр Міллз

У більш складній ситуації у вас може бути випадок, коли fooListє змінною екземпляра, і ви викликаєте метод під час циклу, який в кінцевому підсумку викликає інший метод того ж класу, що і fooList.remove(obj). Бачили, як це відбувається. У цьому випадку копіювання списку найбезпечніше.
Дейв Гріффітс

Відповіді:


419

Дозвольте навести кілька прикладів з деякими альтернативами, щоб цього не було 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яка має підтримку для видалення та додавання елементів під час самої ітерації.

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Недоліком у зборі та підході є те, що нам доводиться повторювати двічі. Спочатку ми повторюємо, шукаючи об’єкт, який відповідає нашим критеріям видалення, і, як тільки ми його знайшли, ми просимо видалити його з оригінальної колекції, що означало б повторну роботу ітерації для пошуку цього елемента, щоб видали це.
  • Я думаю, що варто згадати, що метод видалення Iteratorінтерфейсу позначений як "необов'язковий" у Javadocs, це означає, що можуть бути Iteratorреалізації, які кидають, UnsupportedOperationExceptionякщо ми викликаємо метод видалення. Як такий, я б сказав, що цей підхід менш безпечний, ніж інші, якщо ми не можемо гарантувати підтримку ітератора для видалення елементів.

Браво! це остаточне керівництво.
Magno C

Це ідеальна відповідь! Дякую.
Вільгельм

7
У параграфі про потоки JDK8, про які ви згадуєте removeAll(filtered). Ярлик для цього будеremoveIf(b -> b.getIsbn().equals(other))
ifloop

Чим відрізняється Iterator від ListIterator?
Олександр Міллс

Не вважався видаленням Якщо це було, але це була відповідь на мої молитви. Дякую!
Акабел


13

Чи є причини віддати перевагу одному підходу над іншим

Перший підхід спрацює, але має очевидні витрати на копіювання списку.

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

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


На жаль, мається на увазі, що я б використовував метод видалення ітератора, а не контейнер. А скільки накладних даних створює копіювання списку? Це не може бути багато, і оскільки він підходить до методу, сміття має збиратися досить швидко. Див. Редагування ..
user1329572

1
@aix Я думаю, що варто згадати про метод видалення Iteratorінтерфейсу, який позначений як необов'язковий у Javadocs, а це означає, що можуть бути впроваджені Iterator UnsupportedOperationException. Я б сказав, що такий підхід менш безпечний, ніж перший. Залежно від реалізацій, призначених для використання, перший підхід може бути більш підходящим.
Едвін Далорцо

@EdwinDalorzo remove()у самій оригінальній колекції може також кинути UnsupportedOperationException: docs.oracle.com/javase/7/docs/api/java/util/… . На жаль, інтерфейси Java-контейнерів, на жаль, визначені вкрай ненадійними (якщо чесно перемогти точку інтерфейсу). Якщо ви не знаєте точної реалізації, яка буде використовуватися під час виконання, краще робити речі незмінним способом - наприклад, використовувати API Java 8+ Streams для фільтрації елементів і збору їх у новий контейнер, а потім повністю замініть старий.
Матвій

5

Працює лише другий підхід. Ви можете змінювати колекцію під час ітерації iterator.remove()лише за допомогою . Усі інші спроби спричинить ConcurrentModificationException.


2
Перша спроба повторюється на копії, тобто він може змінювати оригінал.
Колін Д

3

Старий улюблений таймер (він все ще працює):

List<String> list;

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

1

Ви не можете зробити друге, тому що навіть якщо ви використовуєте remove()метод на Iterator , ви отримаєте виняток .

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

Третя альтернатива - створити нову Collection, повторити над оригіналом та додати всіх членів першого Collectionдо другого Collection, які не піддаються видаленню. Залежно від розміру Collectionта кількості видалених, це може значно заощадити на пам'яті порівняно з першим підходом.


0

Я вибрав би друге, оскільки вам не потрібно робити копію пам'яті, і Ітератор працює швидше. Так ви економите пам’ять та час.


" Ітератор працює швидше ". Щось підтримати цю вимогу? Крім того, слід пам’яті зробити копію списку дуже тривіально, тим більше, що він буде визначений в рамках методу і буде зібраний сміття майже відразу.
користувач1329572

1
Недоліком першого підходу є те, що нам доводиться повторювати двічі. Ми повторяємо пошук в елементі foor-loop, і як тільки ми його знайдемо, ми просимо видалити його з оригінального списку, що означало б другу роботу ітерації для пошуку цього заданого елемента. Це підтвердило б твердження, що принаймні в цьому випадку ітераторський підхід повинен бути швидшим. Ми повинні врахувати, що створюється лише структурний простір колекції, а об'єкти всередині колекції не копіюються. Обидві колекції зберігали б посилання на однакові об’єкти. Коли 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. Якщо ви збираєтеся до цього, принаймні зробіть це назад, щоб уникнути цієї проблеми. По-друге, не всі реалізації списку пропонують прямий доступ до елементів (як це робить ArrayList). У LinkedList це було б надзвичайно неефективно, тому що кожного разу, коли ви видаєте повідомлення, get(i)ви повинні відвідувати всі вузли, поки не дістанетесь i.
Едвін Далорцо

Ніколи цього не враховував, оскільки зазвичай я просто використовував його для видалення одного шуканого предмета. Добре знати.
Дрейк Кларріс

4
Я спізнююся на вечірку, але напевно у блоці if if після Foo.remove(i);того, як ви повинні це зробити i--;?
Bertie Wheen

тому що його помилили
Джек
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.