Почему Alibaba запрещает операцию удаления/добавления элементов в цикле foreach

Java

GitHub 1.4k ЗвездаПуть к тому, чтобы стать Java-инженером

В Руководстве по разработке Java для Alibaba есть такое положение:

-w1191

Тем не менее, в руководстве не приводится конкретная причина.В этой статье будет подробно проанализировано мышление, лежащее в основе регулирования.

цикл foreach

Цикл Foreach (цикл Foreach) — это оператор потока управления на языке программирования, который обычно используется для перебора элементов в массиве или коллекции.

В языке Java появились циклы foreach, начиная с JDK 1.5.0. Foreach предоставляет разработчикам очень удобное перемещение по массивам и коллекциям.

Синтаксис foreach следующий:

for(元素类型t 元素变量x : 遍历对象obj){ 
     引用了x的java语句; 
} 

Следующие примеры демонстрируют использование обычных циклов for и foreach:

public static void main(String[] args) {
    // 使用ImmutableList初始化一个List
    List<String> userNames = ImmutableList.of("Hollis", "hollis", "HollisChuang", "H");

    System.out.println("使用for循环遍历List");
    for (int i = 0; i < userNames.size(); i++) {
        System.out.println(userNames.get(i));
    }

    System.out.println("使用foreach遍历List");
    for (String userName : userNames) {
        System.out.println(userName);
    }
}

Вывод приведенного выше кода:

使用for循环遍历List
Hollis
hollis
HollisChuang
H
使用foreach遍历List
Hollis
hollis
HollisChuang
H

Можно видеть, что при использовании синтаксиса foreach для обхода коллекции или массива он может иметь тот же эффект, что и обычный цикл for, и код становится более кратким. Поэтому цикл foreach также часто называют расширенным циклом for.

Однако, как квалифицированному программисту, нам нужно не только знать, что такое расширенный цикл for, но и каков принцип расширенного цикла for?

По сути, расширенный цикл for также является синтаксическим сахаром, предоставляемым Java.Если файл класса, скомпилированный с приведенным выше кодом, декомпилировать (с помощью инструмента jad), можно получить следующий код:

Iterator iterator = userNames.iterator();
do
{
    if(!iterator.hasNext())
        break;
    String userName = (String)iterator.next();
    if(userName.equals("Hollis"))
        userNames.remove(userName);
} while(true);
System.out.println(userNames);

Можно обнаружить, что исходный расширенный цикл for на самом деле реализован с использованием цикла while и итератора. (Запомните эту реализацию, она будет использоваться позже!)

Проблема воспроизводится

В спецификации указано, что нам не разрешено добавлять/удалять элементы коллекции в цикле foreach, поэтому давайте попробуем и посмотрим, что получится.

// 使用双括弧语法(double-brace syntax)建立并初始化一个List
List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

for (int i = 0; i < userNames.size(); i++) {
    if (userNames.get(i).equals("Hollis")) {
        userNames.remove(i);
    }
}

System.out.println(userNames);

В приведенном выше коде сначала используется синтаксис с двойными скобками для создания и инициализации списка, который содержит четыре строки, а именно Hollis, hollis, HollisChuang и H.

Затем используйте обычный цикл for для обхода списка и удалите элементы в списке, содержимое которых равно Hollis. Затем выведите List, вывод будет следующим:

[hollis, HollisChuang, H]

В приведенном выше примере обычный цикл for используется для удаления при обходе, а затем посмотрим, что произойдет, если использовать расширенный цикл for:

List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

for (String userName : userNames) {
    if (userName.equals("Hollis")) {
        userNames.remove(userName);
    }
}

System.out.println(userNames);

В приведенном выше коде используется расширенный цикл for для перебора элементов и попытки удалить из него строковый элемент Холлиса. Выполнение приведенного выше кода вызывает следующее исключение:

java.util.ConcurrentModificationException

Точно так же читатели могут попытаться использовать метод add для добавления элементов в расширенный цикл for, и результат также вызовет это исключение.

Причина, по которой возникает это исключение, заключается в том, что срабатывает механизм обнаружения ошибок коллекции Java - отказоустойчивость.

fail-fast

Далее разберем, почему выбрасывается java.util.ConcurrentModificationException при добавлении/удалении элементов в расширенном цикле for, то есть объясним, что такое отказоустойчивая система, принцип отказоустойчивости и т. д.

fail-fast или fail fast — это механизм обнаружения ошибок для коллекций Java. Когда несколько потоков выполняют структурные изменения в коллекции (не отказоустойчивый класс коллекции), может возникнуть отказоустойчивый механизм, и в это время будет выдано исключение ConcurrentModificationException (когда метод обнаруживает одновременную модификацию объекта, но это исключение выбрасывается, когда такая модификация не разрешена).

В то же время следует отметить, что даже если это не многопоточная среда, при нарушении правил одним потоком также может быть выдано исключение.

Итак, как удаление элемента в расширенном цикле for нарушает правила?

Чтобы проанализировать эту проблему, мы сначала избавимся от синтаксического сахара, улучшающего цикл for, и получим следующий код:

public static void main(String[] args) {
    // 使用ImmutableList初始化一个List
    List<String> userNames = new ArrayList<String>() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    Iterator iterator = userNames.iterator();
    do
    {
        if(!iterator.hasNext())
            break;
        String userName = (String)iterator.next();
        if(userName.equals("Hollis"))
            userNames.remove(userName);
    } while(true);
    System.out.println(userNames);
}

Затем запустите приведенный выше код, будет выдано такое же исключение. Давайте посмотрим на полный стек ConcurrentModificationException:

-w738

Через стек исключений мы можем добраться до строки 23 цепочки вызовов ForEachDemo, где произошло исключение,Iterator.nextназываетсяIterator.checkForComodificationметод, а исключение генерируется в методе checkForCommodification.

Фактически, после отладки мы можем обнаружить, что если код удаления не был выполнен, строка iterator.next никогда не сообщала об ошибке. Время генерации исключения также является вызовом следующего метода после выполнения удаления.

Давайте посмотрим непосредственно на код метода checkForCommodification и посмотрим, почему выбрасывается исключение:

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

Код относительно прост,modCount != expectedModCount, это броситConcurrentModificationException.

Итак, давайте посмотрим, как операционная комната удаления/добавления приводит к тому, что modCount и ожидаемыйModCount не равны.

что делает удаление/добавление

Прежде всего, мы должны выяснить, что представляют собой две переменные modCount и expectModCount.

Просматривая исходный код, мы можем найти:

  • modCount — это переменная-член в ArrayList. Он представляет собой количество фактических изменений коллекции.
  • ожидаемыйModCount — это внутренний класс в ArrayList — переменная-член в Itr. ожидаемыйModCount представляет количество раз, когда этот итератор ожидает, что коллекция будет изменена. Его значение инициализируется при вызове метода ArrayList.iterator. Это значение изменится только в том случае, если коллекция обрабатывается через итератор.
  • Itr — это реализация Iterator.Итератор, который можно получить с помощью метода ArrayList.iterator, является экземпляром класса Itr.

Отношения между ними следующие:

class ArrayList{
    private int modCount;
    public void add();
    public void remove();
    private class Itr implements Iterator<E> {
        int expectedModCount = modCount;
    }
    public Iterator<E> iterator() {
        return new Itr();
    }
}

На самом деле, видя это, наверное, многие догадываются, почему после операции удаления/добавления ожидаемые ModCount и modCount не хотят ждать.

Просматривая код, мы также можем обнаружить, что основная логика метода удаления выглядит следующим образом:

-w764

Как видите, он изменяет только modCount и ничего не делает с ожидаемым ModCount.

Подводя краткий итог, причина, по которой выбрасывается ConcurrentModificationException, заключается в том, что наш код использует расширенный цикл for, а в расширенном цикле for обход коллекции осуществляется через итератор, но добавление/удаление элементов используется непосредственно собственным классом Collection методы. Это приводит к тому, что итератор обнаруживает, что элемент был удален/добавлен по незнанию, когда он проходит, и будет выдано исключение, чтобы напомнить пользователю, что могла произойти одновременная модификация!

правильная осанка

До сих пор мы объясняли причины, по которым мы не можем напрямую выполнять операции добавления/удаления коллекций в теле цикла foreach.

Однако во многих случаях нам нужно отфильтровать коллекцию, например, удалить некоторые элементы, так как же нам это сделать? Существует несколько способов справки:

1. Используйте итератор напрямую для работы

Помимо прямого использования обычного цикла for, мы также можем напрямую использовать метод удаления, предоставляемый Iterator.

    List<String> userNames = new ArrayList<String>() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    Iterator iterator = userNames.iterator();

    while (iterator.hasNext()) {
        if (iterator.next().equals("Hollis")) {
            iterator.remove();
        }
    }
    System.out.println(userNames);

Если вы используете метод удаления, предоставляемый Iterator напрямую, вы можете изменить значение ожидаемого модкаунта. Тогда больше исключений не будет. Его код реализации выглядит следующим образом:

-w375

2. Используйте фильтр, предоставленный в Java 8, для фильтрации

В Java 8 коллекцию можно преобразовать в поток, и для потока есть операция фильтрации, которая может выполнять определенный тест на исходном потоке, а элементы, прошедшие проверку, остаются для генерации нового потока.

    List<String> userNames = new ArrayList<String>() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    userNames = userNames.stream().filter(userName -> !userName.equals("Hollis")).collect(Collectors.toList());
    System.out.println(userNames);

3. Также можно использовать расширенные циклы for

Если мы абсолютно уверены, что в наборе удаляемый элемент содержит только один элемент, например, при работе с набором, то мы действительно можем использовать расширенный цикл for, если тело цикла завершается сразу после удаления, а не continue Достаточно пройти, то есть не дать коду выполниться до следующего следующего метода.

    List<String> userNames = new ArrayList<String>() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    for (String userName : userNames) {
        if (userName.equals("Hollis")) {
            userNames.remove(userName);
            break;
        }
    }
    System.out.println(userNames);

4. Используйте отказоустойчивый класс коллекции напрямую

В Java, в дополнение к некоторым обычным классам коллекций, есть также некоторые классы коллекций, использующие отказоустойчивый механизм. Такой контейнер коллекции не имеет прямого доступа к содержимому коллекции во время обхода, а сначала копирует исходное содержимое коллекции и проходит по скопированной коллекции.

Поскольку копия исходной коллекции просматривается во время итерации, изменения, внесенные в исходную коллекцию в процессе обхода, не могут быть обнаружены итератором, поэтому ConcurrentModificationException не будет инициировано.

ConcurrentLinkedDeque<String> userNames = new ConcurrentLinkedDeque<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

for (String userName : userNames) {
    if (userName.equals("Hollis")) {
        userNames.remove();
    }
}

Преимущество контента, основанного на копировании, заключается в том, чтобы избежать ConcurrentModificationException, но точно так же итератор не может получить доступ к измененному контенту, то есть итератор проходит копию коллекции, полученную в момент обхода, и модификацию оригинала. сбор происходит во время обхода, итераторы ничего не знают.

Все контейнеры в пакете java.util.concurrent защищены от сбоев и могут использоваться и изменяться одновременно в нескольких потоках.

Суммировать

Усовершенствованный цикл for, который мы используем, на самом деле является синтаксическим сахаром, предоставляемым Java, и его принцип реализации заключается в обходе элементов с помощью Iterator.

Однако, если в процессе обхода коллекция добавляется/удаляется не через Iterator, а через методы самого класса коллекции. Затем, когда итератор выполняет следующий обход, обнаруживается, что существует операция модификации коллекции, которая не выполняется сама по себе, тогда она может быть одновременно выполнена другими потоками.В это время будет выдано исключение, чтобы напомнить пользователь, что возможное возникновение Это так называемый отказоустойчивый механизм.

Конечно, есть много способов решить эту проблему. Например, используйте Iterator для удаления элементов, используйте фильтр Stream, используйте отказоустойчивые классы и т. д.

GitHub 1.4k ЗвездаПуть к тому, чтобы стать Java-инженером, почему бы тебе не прийти и не узнать?

GitHub 1.4k ЗвездаПуть к тому, чтобы стать Java-инженером, ты правда не хочешь узнать?

GitHub 1.4k ЗвездаПуть к тому, чтобы стать Java-инженером, ты действительно уверен, что не хочешь узнать?