Избавляемся от ConcurrentModificationException
Как известно, ConcurrentModificationException к многопоточности никакого отношения не имеет. Возникает эта гадость, когда мы пытаемся модифицировать коллекцию во время итерирования по ней. Как обычно, это имеет исторические корни: коллекции и итераторы появились в Java 1.2, в те времена избежать явного использования итератора при обходе коллекции было никак нельзя, так что предложение менять коллекцию посредством методов итератора не выглядело совсем ужасным:
Iterator iterator = collection.iterator(); while (iterator.hasNext()) < Object element = iterator.next(); if (iDontLikeThisElement(element)) < iterator.remove(); >>
Не, всё же выглядело. Но никаких других вариантов не было. Позже в пятой джаве появляется цикл foreach, и использование итераторов становится преимущественно неявным:
for (E element : collection) < if (iDonLikeThisElement(element)) < collection.remove(element); // облом! ConcurrentModificationException! >>
«Ишь чего захотели! Юзайте явные итераторы, дорогие кастомеры, и не выделывайтесь» — наверное что-то такое думали разработчики джава платформы работая над пятеркой.
В шестой джаве появляется пакет конкаренси. Теперь можно cделать так:
Set set = Collections.newSetFromMap(new ConcurrentHashMap<>());
И получить set который не кидается ConcurrentModificationException-ами. Но опять же счастье не совсем полное:
- Oбычно многопоточность нам вовсе не нужна
- Не подерживаются null ни в качестве элементов, ни ключей, ни значений. Да и ладно, честно сказать.
- Порядок элементов не определён и может меняться — вот это гораздо хуже. Т.е. если мы бежим по элементам и ведём некий подсчёт с потерей точности, то нас могут поджидать неприятные сюрпризы и разные результаты на одних и тех же наборах данных, что, скажем, не всегда хорошо. Так же бывают задачи, где желательно сохранить именно изначальный порядок данных. Ну и вот такие штуки тоже имеют место быть:
set.add("aaa"); set.add("bbb"); for (String s : set)
aaa
bbb
ddd
set.add("aaa"); set.add("bbb"); for (String s : set)
Поэтому сейчас мы сделаем свою собственную коллекцию с чётким порядком. И так, что мы хотим получить:
- В рамках одного треда можно добавлять и удалять элементы в любой момент без всяких эксепшенов. И конечно же за константное время.
- Можно хранить null-ы, если вдруг хочется.
- Элементы обходятся в том порядке в котором были добавлены.
- Удаляя элемент мы не будем обнулять ссылку на следующий, т. е. eсли итератор стоит на данном элементе, то он сможет пройти дальше.
- В конце списка поместим фэйковый элемент, который превращается в настоящий когда в список что-нибудь добавляют. Т.е. даже добравшись до конца списка итератор не упирается в null и может продолжить работу если в коллекции появляется новый элемент. Далее в коде этот фейковый элемент называется placeholder.

- В начале у нас есть элементы A, B, C, D.
- Затем элементы C и D удаляются.
- Добавляется новый элемент E.
Ну и для константного времени доступа нам, очевидно, нужен хэшмап:
import java.util.AbstractSet; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; public class LinkedSet extends AbstractSet < private static class LinkedElement < E value; boolean exists; LinkedElementprev; LinkedElement next; > private Map map = new HashMap<>(); private LinkedElement placeholder = new LinkedElement<>(); private LinkedElement head = placeholder; @Override public boolean isEmpty() < return head == placeholder; >@Override public int size() < return map.size(); >@Override public boolean contains(Object o) < return map.containsKey(o); >// здесь будут методы для добавления, удаления, итерирования >
@Override public boolean add(E e) < LinkedElementelement = map.putIfAbsent(e, placeholder); if (element != null) < return false; >element = placeholder; element.exists = true; element.value = e; placeholder = new LinkedElement<>(); placeholder.prev = element; element.next = placeholder; return true; >
@Override public boolean remove(Object o) < LinkedElementremovedElement = map.remove(o); if (removedElement == null) < return false; >removeElementFromLinkedList(removedElement); return true; > private void removeElementFromLinkedList(LinkedElement element) < element.exists = false; element.value = null; element.next.prev = element.prev; if (element.prev != null) < element.prev.next = element.next; element.prev = null; >else < head = element.next; >>
@Override public Iterator iterator() < return new ElementIterator(); >private class ElementIterator implements Iterator < LinkedElementnext = head; LinkedElement current = null; LinkedElement findNext() < LinkedElementn = next; while (!n.exists && n.next != null) < next = n = n.next; >return n; > @Override public boolean hasNext() < return findNext().exists; >@Override public E next() < LinkedElementn = findNext(); if (!n.exists) < throw new NoSuchElementException(); >current = n; next = n.next; return n.value; > @Override public void remove() < if (current == null) < throw new IllegalStateException(); >if (map.remove(current.value, current)) < removeElementFromLinkedList(current); >else < throw new NoSuchElementException(); >> >
Теперь можно делать так:
Set set = new LinkedSet<>(); // . put some numbers set.stream().filter(v -> v % 2 == 0).forEach(set::remove);
Понятно, что аналогично можно сконструировать и LinkedMap. Вот в общем-то и всё, ещё один велосипед готов. Почему подобным образом не доработали библиотечные LinkedHashMap и LinkedHashSet? Кто знает, возможно чтобы джависты завидовали джаваскриптистам.
- java
- коллекции
- ConcurrentModificationException
Как избежать ConcurrentModificationException при пользовании коллекциями
Название ConcurrentModificationException многих вводит в заблуждение. При слове Concurrent первое что приходит на ум — многотредность. Однако, данное исключение относится вовсе не к многотредности. Исключение может происходить при работе с коллекциями при обычной однотредной работе.
Для «прохода» по коллекции используются структуры данных, реализующие паттерн «View». Структуры данных эти называются итераторами и могут использоваться явно и не явно. Неявно итераторы используются при использовании конструкции foreach.
ConcurrentModificationException возникает когда коллекция модифицируется «одновременно» с проходом по коллекции итератором любыми средствами кроме самого итератора.
Например, ниже при удалении элемента из map произойдет ConcurrentModificationException, поскольку в цикле for неявно формируется итератор, и из map удаляется элемент, в процессе прохода по map.
@Test(expected = ConcurrentModificationException.class) public void testForEachFail() < Mapmap = new HashMap(); map.put("a", "a"); map.put("b", "b"); for(String key:map.keySet()) < map.remove(key); >>
Заблуждения добавляет тот факт, что в случае, если в map в приведенном примере будет лишь один элемент, то ConcurrentModificationException не возникнет (ниже тот же код, но добавляется лишь один элемент в map):
@Test public void testForEachOneElement() < Mapmap = new HashMap(); map.put("a", "a"); for(String key:map.keySet()) < map.remove(key); >>
Тест выше проходит, не смотря на то, что не верен… Еще один пример неверного кода, уже со StackOverflow:
// @see http://stackoverflow.com/questions/602636/concurrentmodificationexception-and-a-hashmap Iterator it = map.entrySet().iterator(); while (it.hasNext())
В вышеприведенном примере, СoncurrentModificationException возникает при явном использовании итератора для прохода по элементам. Но в то же самое время элемент удаляется по ключу: map.remove(item.getKey()).
Как же сделать удаление правильно? Правильно, нужно использовать метод итератора, для удаления элемента:
// @see http://stackoverflow.com/questions/602636/concurrentmodificationexception-and-a-hashmap Iterator it = map.entrySet().iterator(); while (it.hasNext())
Краткое summary
ConcurrentModificationException не всегда является следствием неверной синхронизации работы с коллекциями. ConcurrentModificationException возникает при изменении коллекции любыми средствами, отличными от итератора, при проходе по коллекции с помощью итератора. Изменение такое, как продемонстрировано выше может происходить и в одно-тредной среде. Не лишне также добавить, что исключение может возникать не только при удалении, но и при добавлении элемента, а также что модификация контейнера может возникать и в многотредной среде. Так что исключение не обязательно тривиальное. Однако, прежде чем пугаться, есть повод проверить а не является ли это одним из приведенных выше антипаттернов.
Надежность библиотек Java усыпляет. Описанные выше примеры показывают, что в Java все таки есть способы «стрельнуть себе в ногу».
Избегаем ConcurrentModificationException при удалении элементов из ArrayList во время итерации
Одной из распространенных проблем, с которыми сталкиваются разработчики на Java, является ConcurrentModificationException. Это исключение возникает, когда коллекция, по которой происходит итерация, изменяется в процессе этой итерации. Проблема становится особенно актуальной, когда нужно удалить элемент из списка во время итерации.
Пример кода, который вызывает это исключение:
ArrayList<String> list = new ArrayList<String>(); list.add("Java"); list.add("PHP"); list.add("Python"); for (String language : list) < if (language.equals("PHP")) < list.remove(language); >>
В этом примере попытка удалить элемент списка во время итерации через него вызывает ConcurrentModificationException.
Чтобы избежать этого исключения, можно использовать итератор и метод remove(). Итератор предоставляет безопасный способ удаления элементов из коллекции во время итерации.
Пример кода, который безопасно удаляет элемент из списка во время итерации:
ArrayList<String> list = new ArrayList<String>(); list.add("Java"); list.add("PHP"); list.add("Python"); Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) < String language = iterator.next(); if (language.equals("PHP")) < iterator.remove(); >>
Таким образом, для безопасного удаления элементов из коллекции во время итерации по ней следует использовать итератор и его метод remove().
ConcurrentModificationException
Вот ещё что странно из лекции 2 уровень 8. https://javarush.com/quests/lectures/questsyntax.level08.lecture02 — В Java есть сокращённая запись работы с итераторами. По аналогии с while, в for был добавлен еще один специальный оператор «for each» — «для каждого». Обозначается тоже ключевым словом for. — Оператор for each используется только при работе с коллекциями и контейнерами. В нем неявно используется итератор, но мы видим уже полученный элемент. у нас если у for неявно используется итератор — почему нельзя удалять/добавлять элемент? Или это такой мини-итератор только чтобы пройтись по всем элементам коллекции, даже если у них нет номера (Set и Map)
Wladyslaw Java Developer Master
19 июня 2019, 20:36
Знаю что нельзя модифицировать коллекцию напрямую когда по ней итерируешь — только через методы итератора. Логически понятно почему так — курсор в итераторе тогда не понятно на что указывать будет. А как оно «под капотом устроено» — сам пока не разобрался
Уровень 25
16 июня 2019, 09:47
Лекция на эту тему (вторая часть): https://javarush.com/groups/posts/1935-udalenie-ehlementa-iz-spiska-arraylist И годное описание со StackOverFlow: https://ru.stackoverflow.com/questions/454175/%D0%9A%D0%B0%D0%BA-%D0%BE%D0%B1%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%B0%D1%82%D1%8C-%D0%B8%D1%81%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%BD%D0%B8%D0%B5-java-util-concurrentmodificationexception
Уровень 20
19 июня 2019, 20:30
спасибо, из этих лекций я понял, что всё дело в цикле foreach однако исключение возникает даже в явном использовании итератора(((
- Курсы программирования
- Регистрация
- Курс Java
- Помощь по задачам
- Цены
- Задачи-игры
Сообщество
JavaRush — это интерактивный онлайн-курс по изучению Java-программирования c нуля. Он содержит 1200 практических задач с проверкой решения в один клик, необходимый минимум теории по основам Java и мотивирующие фишки, которые помогут пройти курс до конца: игры, опросы, интересные проекты и статьи об эффективном обучении и карьере Java‑девелопера.