ConcurrentModificationException иногда выкидывается, а иногда нет
Заметил одну особенность.
public static void main(String args[]){
List<Integer> list = new ArrayList<>();
list.add(10);
list.add(20);
list.add(30);
list.add(40);
list.add(50);
for(int ints : list){
if(ints == 30){
list.remove(new Integer(ints));
}
}
}
Как и ожидается, в ран-тайме выкидывается ConcurrentModificationException. (Т.к. насколько мне известно enchanced for под капотом работает на итераторе, как-то связано с полем snapshot) Но! Если мы немного изменим данный пример, то все прекрасно работает.
public static void main(String args[]){
List<Integer> list = new ArrayList<>();
list.add(10);
list.add(20);
list.add(30);
for(int ints : list){
if(ints == 20){
list.remove(new Integer(ints));
}
}
}
Сейчас всё прекрасно работает. Почему так происходит? В одном случае выкидывается исключение, а в другом нет! Проверяю на Java 8. Прошу дать исчерпывающий ответ)
Ответы (2 шт):
Исчерпывающий ответ заключается в том, что так не нужно делать в принципе. Используйте list.removeIf
list.removeIf(item -> item == 20);
Так происходит из-за того, что удаляется предпоследний элемент в списке.
Цикл for (int ints : list) неявно использует итератор списка и фактически эквивалентен следующему коду:
for (Iterator<Integer> i = ints.iterator(); i.hasNext();) {
int ints = i.next().intValue(); // unboxing
// ...
}
Этот итератор может быть реализован во внутреннем классе Itr, пример в JDK8
Метод remove(Object x) для ArrayList вообще НЕ использует итератор, а просто меняет размер списка size. Например, в JDK8 он реализован так
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
/*
* Private remove method that skips bounds checking and does not
* return the value removed.
*/
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
Соответственно метод итератора hasNext в случае удаления предпоследнего элемента вернёт false:
public boolean hasNext() {
return cursor != size;
}
и метод next(), в котором, собственно вызывается проверка на одновременную модификацию, не будет вызван, так как цикл уже закончится.
При попытке удалить другие (не предпоследние) элементы методом List::remove во время итерации по списку, будет выброшено исключение, так как cursor != size == true.
По этому поводу неоднократно открывались баги в JDK:
- JDK-8136821: ConcurrentModificationException not thrown when removing the next to last element, less than expected number of iterations
- JDK-8210207: Concurrent Modification Exception not thrown when removing second last element from list
которые как правило закрывались так как выбрасывание такого рода исключения не гарантируется в fail-fast итераторах, о чём указано в документации API:
JavaDoc JDK 8 java.util.ArrayList:
The iterators returned by this class's
iteratorandlistIteratormethods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw aConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw
ConcurrentModificationExceptionon a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.