Пожалуйста, найдите мои комментарии к вашим предположениям ниже.
Я понимаю, что ConcurrentModificationException
генерируется всякий раз, когда после создания итератора происходит структурная модификация базовой коллекции.
Не совсем. ConcurrentModificationException
фактически выбрасывается, когда итератор пытается извлечь следующее значение, и в базовой коллекции произошли структурные изменения. (Хочу подчеркнуть, что ConcurrentModificationException
не выбрасывается при вызове, т.е. add
или remove
для базовой коллекции).
Тогда относительно вашего последнего предположения:
В конечном счете, коллекция обновляется, и если создается итератор, обход может все же привести к противоречивым результатам.
Я не думаю, что это правильно. Как обход может все еще привести к противоречивым результатам? Если вы измените значение в i-й позиции списка, вы сразу увидите это изменение. Список фактически изменен, но не структурно изменен. Изменяется только значение в i-й позиции, но не структура списка (т. Е. Базовый массив ArrayList
не будет подвергаться риску изменения размера, LinkedList
не будет выделять новый узел и т. Д.) .
Наконец, в отношении этого предложения:
Это похоже на проектное решение.
Абсолютно. Это на 100% дизайнерское решение, и хотя я не был его частью, я уверен, что оно было обусловлено целью избежать несоответствий при обходе базовой коллекции. Лучше, чем в конечном итоге получить неожиданные или противоречивые данные, как можно раньше выдать исключение и сообщить пользователю.
Однако в документах также упоминается, что безотказные итераторы выбрасывают ConcurrentModificationException
на максимальных усилиях .
Например, рассмотрим следующий код:
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
try {
Integer next = iterator.next();
System.out.println("iterator.next() = " + next);
System.out.println("BEFORE remove: " + list);
list.remove(0);
System.out.println("AFTER remove: " + list);
} catch (ConcurrentModificationException e) {
System.out.println("CME thrown, ending abnormally...");
System.exit(1);
}
}
Здесь мы пытаемся удалить первый элемент списка, перебирая его. После выполнения вы увидите следующий вывод:
iterator.next() = 1
BEFORE remove: [1, 2, 3]
AFTER remove: [2, 3]
CME thrown, ending abnormally...
Это ожидаемое поведение. ConcurrentModificationException
генерируется, когда итератор пытается извлечь элемент из списка во второй раз. Это потому, что реализация обнаруживает, что структурное изменение было сделано с момента создания итератора. Таким образом, iterator.next()
выбрасывает CME.
Однако, поскольку fail-fast итераторы не всегда гарантированно выдают ConcurrentModificationException
, но сделать это только на основе максимальных усилий, есть способ обмануть реализацию. Рассмотрим приведенный ниже код, который является модифицированной версией предыдущего фрагмента (если вы выполните его, имейте в виду, что до его завершения потребуется некоторое время):
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
int structuralChanges = 0;
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
try {
Integer next = iterator.next();
System.out.println("iterator.next() = " + next);
System.out.println("BEFORE remove: " + list);
list.remove(0);
System.out.println("AFTER remove: " + list);
structuralChanges++;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
list.add(i);
structuralChanges++;
list.remove(list.size() - 1);
structuralChanges++;
}
list.add(0);
structuralChanges++;
System.out.println("Structural changes so far = " + structuralChanges);
} catch (ConcurrentModificationException e) {
System.out.println("CME NEVER thrown, so you won't see this message");
System.exit(1);
}
}
System.out.println("AFTER everything: " + list);
System.out.println("Program ending normally");
Здесь мы снова удаляем первый элемент списка во время его итерации, но перед тем, как итератор выберет следующий элемент, мы делаем миллионы структурных изменений. Фактически, мы делаем Integer.MAX_VALUE * 2 + 1
структурные модификации, если мы рассмотрим все вызовы list.add(i)
и list.remove(list.size() - 1)
внутри цикла for
вместе с вызовом list.add(0)
сразу после цикла for
.
Кроме того, мы отслеживаем все структурные изменения с помощью переменной structuralChanges
, которая увеличивается сразу после каждой структурной модификации.
После выполнения кода выше вы увидите следующий вывод:
iterator.next() = 1
BEFORE remove: [1, 2, 3]
AFTER remove: [2, 3]
Structural changes so far = 0
iterator.next() = 3
BEFORE remove: [2, 3, 0]
AFTER remove: [3, 0]
Structural changes so far = 0
iterator.next() = 0
BEFORE remove: [3, 0, 0]
AFTER remove: [0, 0]
Structural changes so far = 0
AFTER everything: [0, 0, 0]
Program ending normally
Как показывает вывод, ConcurrentModificationException
не было выброшено.
Кроме того, после всех структурных изменений значение переменной structuralChanges
в итоге становится 0
. Это потому, что это переменная типа int
, и она переполняется и снова достигает 0
, после увеличения ее * в 1078 * раз (Integer.MAX_VALUE * 2 + 1
раз из-за нашего искусственного цикла for
и последующего приращения плюс 1
из-за list.remove(0)
вызова исходного кода).
Затем, после всех этих Integer.MAX_VALUE * 2 + 2
структурных модификаций, когда мы вызываем iterator.next()
на следующей итерации цикла while
, ConcurrentModificationException
не выбрасывается, никогда. Мы обманули реализацию, поэтому теперь мы можем видеть, что происходит с данными внутри. (Внутренне реализация отслеживает структурные изменения, сохраняя счет int
, как я делал с моей переменной structuralChanges
).