Почему критерии проектирования для ConcurrentModificationException выбраны только для структурной модификации - PullRequest
0 голосов
/ 30 октября 2018

Я понимаю, что ConcurrentModificationExcpetion генерируется всякий раз, когда существует структурная модификация базовой коллекции после создания итератора. Это похоже на дизайнерское решение.

Мне было интересно, почему обновление через методы set () не рассматривается для CME? В конечном счете, коллекция обновляется, и, если итератор создан, обход может привести к противоречивым результатам.

Ответы [ 3 ]

0 голосов
/ 30 октября 2018

В конечном счете, коллекция обновляется, и если создается итератор, обход может по-прежнему приводить к противоречивым результатам.

На самом деле, я думаю, что причина в том, что set операции не будут приводить к противоречивым результатам. Код, повторяющий коллекцию, будет либо видеть результаты модификации вызова set ... либо нет ... в очень предсказуемой манере. И код гарантированно увидит каждый элемент коллекции (т. Е. Кроме того, который вы «установили») ровно один раз.

В отличие от этого, структурные изменения в типичных не параллельных коллекциях могут привести к перемещению записей / элементов коллекции. Например:

  • Вставка или удаление в ArrayList заставляет элементы в массиве поддержки менять положение.

  • Вставка в HashMap или HashSet может привести к изменению размера, что приведет к реконструкции цепочек хеширования.

В обоих случаях структурная модификация может привести к пропуску элементов или их просмотру более одного раза.

0 голосов
/ 30 октября 2018

Пожалуйста, найдите мои комментарии к вашим предположениям ниже.

Я понимаю, что 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).

0 голосов
/ 30 октября 2018

Невозможно узнать точные причины проектного решения, если вы на самом деле не участвовали в их принятии.

Я предполагаю, что причина может заключаться в том, что итератор предназначен для итерации коллекции , а не значений в коллекции .

Другими словами, итератор подобен «указателю», который будет просто перемещаться, чтобы указывать на каждое «пространство» в коллекции по очереди. Зная кое-что о «форме» коллекции при создании итератора, вы знаете, как переместить указатель, чтобы указать следующий пробел в коллекции.

Если значение, сохраненное в пробеле, изменяется, это не имеет значения: этот пробел и все остальные пробелы в коллекции остаются без изменений.

Однако, если вы измените форму коллекции с помощью структурной модификации, вы в основном обесценили любые ваши предположения о том, куда указывает этот указатель; если не делать что-то очень умное, вы можете просто сдаться и выбросить исключение.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...