Итерация по коллекции: избегаем ConcurrentModificationException при удалении объектов в цикле
Описание проблемы:
Мы все знаем, что нельзя делать следующее из-за исключения ConcurrentModificationException
:
for (Object i : l) {
if (condition(i)) {
l.remove(i);
}
}
Однако на практике иногда это срабатывает, а иногда — нет. Вот конкретный пример кода:
public static void main(String[] args) {
Collection<Integer> l = new ArrayList<>();
for (int i = 0; i < 10; ++i) {
l.add(4);
l.add(5);
l.add(6);
}
for (int i : l) {
if (i == 5) {
l.remove(i);
}
}
System.out.println(l);
}
В данном случае это приводит к следующему результату:
Exception in thread "main" java.util.ConcurrentModificationException
Хотя несколько потоков не участвуют в этом процессе.
Каково лучшее решение этой проблемы? Как можно удалить элемент из коллекции в цикле, не вызывая это исключение?
Также стоит отметить, что я использую произвольную коллекцию, а не обязательно ArrayList
, так что нельзя полагаться на метод get
.
5 ответ(ов)
Да, это действительно работает. В приведенном вами коде используется явный итератор для обхода списка. Идея заключается в том, что метод remove()
позволяет безопасно удалять элементы во время итерации, что невозможно сделать с помощью цикла for-each
, так как он не предоставляет такой функциональности.
Пример:
Iterator<Integer> iter = l.iterator();
while (iter.hasNext()) {
if (iter.next() == 5) {
iter.remove();
}
}
В этом коде мы создаем итератор и с помощью цикла while
проходим по всем элементам списка. Если текущий элемент равен 5, мы используем метод remove()
итератора для его удаления. Это важно, так как попытка удалить элемент непосредственно из списка во время итерации с помощью цикла for-each
приведёт к ConcurrentModificationException
.
Таким образом, хотя цикл for-each
и удобен для обхода коллекции, использование явного итератора позволяет вам изменять коллекцию безопасно во время итерации.
Поскольку на этот вопрос уже был дан ответ, а именно, что лучший способ — использовать метод remove
объекта итератора, я хотел бы подробнее остановиться на том месте, где возникает ошибка "java.util.ConcurrentModificationException"
.
Каждый класс коллекции имеет внутренний приватный класс, который реализует интерфейс Iterator и предоставляет методы, такие как next()
, remove()
и hasNext()
.
Код метода next
выглядит примерно так:
public E next() {
checkForComodification();
try {
E next = get(cursor);
lastRet = cursor++;
return next;
} catch(IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
Метод checkForComodification
реализован следующим образом:
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
Как видно, если вы попытаетесь явно удалить элемент из коллекции, это приведет к тому, что значение modCount
станет отличным от expectedModCount
, что, в свою очередь, вызовет исключение ConcurrentModificationException
.
Для избежания этой ошибки рекомендуется использовать метод remove
в самом итераторе, который корректно обновляет внутреннее состояние коллекции и исключает возможность возникновения вышеупомянутого исключения.
Вы можете использовать итератор напрямую, как вы упомянули, или же создать вторую коллекцию и добавлять в неё каждый элемент, который вы хотите удалить, а затем в конце вызвать метод removeAll
. Это позволит вам сохранить типобезопасность цикла for-each
, однако приведёт к увеличению использования памяти и времени ЦП (что не должно стать серьёзной проблемой, если у вас не очень большие списки или очень старый компьютер).
Вот пример кода:
public static void main(String[] args) {
Collection<Integer> l = new ArrayList<Integer>();
Collection<Integer> itemsToRemove = new ArrayList<>();
for (int i = 0; i < 10; i++) {
l.add(Integer.valueOf(4));
l.add(Integer.valueOf(5));
l.add(Integer.valueOf(6));
}
for (Integer i : l) {
if (i.intValue() == 5) {
itemsToRemove.add(i);
}
}
l.removeAll(itemsToRemove);
System.out.println(l);
}
В этом примере мы сначала создаем основной список l
и коллекцию itemsToRemove
, куда будем добавлять элементы для удаления. После этого проходим по элементам списка и добавляем в itemsToRemove
те элементы, которые равны 5
. В конце мы удаляем все элементы, собранные в itemsToRemove
, из основного списка и выводим результат.
В таких случаях распространённый подход заключается в том, чтобы пройтись по списку в обратном порядке:
for(int i = l.size() - 1; i >= 0; i--) {
if (l.get(i) == 5) {
l.remove(i);
}
}
Тем не менее, я очень рад, что в Java 8 появились более удобные способы, такие как removeIf
или использование filter
в потоках.
Чтобы создать копию существующего списка и итерироваться по новому примеру, вы можете использовать конструктор ArrayList
, который принимает в качестве аргумента оригинальный список. В вашем случае, если вы хотите удалить элементы из listOfStr
, итерируясь по его копии, это можно сделать следующим образом:
for (String str : new ArrayList<>(listOfStr)) {
listOfStr.remove(str);
}
Здесь мы создаем новый список ArrayList
, который является копией listOfStr
, и итерируемся по нему. Таким образом, вы сможете безопасно изменять оригинальный список listOfStr
, не вызывая ConcurrentModificationException
. Обратите внимание, что в этом примере мы удаляем каждый элемент, который итерируем, из оригинального списка.
Эффективный способ итерации по каждой записи в Java Map?
Инициализация ArrayList в одну строчку
Преобразование 'ArrayList<String>' в 'String[]' в Java
Сортировка Map<Key, Value> по значениям
Разница между <? super T> и <? extends T> в Java