Почему руководство по разработке Alibaba заставляет не выполнять операцию удаления в foreach

Java задняя часть

«Это первый день моего участия в ноябрьском испытании обновлений, ознакомьтесь с подробностями события:Вызов последнего обновления 2021 г."

В тот день Сяо Эр отправился к Али на собеседование, и интервьюер Лао Ван задал ему вопрос, как только тот появился: Почему в Руководстве по разработке Java для Али принудительно не удаляются элементы в foreach? Прослушав его, Сяо Эр обрадовался, ведь два года назад, в 2021 году, он увидел этот вопрос в 63-й статье рубрики «Дорога к продвинутым Java-программистам» 😆.

PS: Вы можете только просить такую ​​​​звезду, не запрашивая ее, это не будет иметь никакого эффекта.Айронс, «Дорога к продвинутым программистам Java» уже получил 523 звезды на GitHub звезда!

GitHub.com/IT Ван Эр/до…


Ради градостроительства сначала переместите абзац на английском языке, чтобы объяснить отказоустойчивость.

In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system's state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.

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

public void test(Wanger wanger) {   
    if (wanger == null) {
        throw new RuntimeException("wanger 不能为空");
    }
    
    System.out.println(wanger.toString());
}

Как только обнаруживается, что Вангер имеет значение null, немедленно генерируется исключение, и вызывающая сторона может решить, что делать в этом случае, следующий шагwanger.toString()не будет выполняться - во избежание более серьезных ошибок.

Много раз мы классифицировали отказоустойчивость как механизм обнаружения ошибок Java Collections Framework, но на самом деле отказоустойчивость не является механизмом, специфичным для Java Collections Framework.

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

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");

for (String str : list) {
	if ("沉默王二".equals(str)) {
		list.remove(str);
	}
}

System.out.println(list);

Этот код, похоже, не имеет никаких проблем, но при запуске он выдает ошибку.

Согласно неверной информации о стеке, мы можем найти строку 901 ArrayList.

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

То есть, когда запускается удаление, запускается выполнение.checkForComodificationметод, который сравнивает modCount и ожидаемыйModCount, обнаруживает, что они не равны, и выдаетConcurrentModificationExceptionаномальный.

почему это выполняетсяcheckForComodificationметод?

Это потому, что for-each по сути является синтаксическим сахаром, а нижний слой сквозной.Итератор ИтераторРеализованный с помощью цикла while, давайте посмотрим на декомпилированный байт-код.

List<String> list = new ArrayList();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
Iterator var2 = list.iterator();

while(var2.hasNext()) {
    String str = (String)var2.next();
    if ("沉默王二".equals(str)) {
        list.remove(str);
    }
}

System.out.println(list);

Взгляните на метод итератора ArrayList:

public Iterator<E> iterator() {
    return new Itr();
}

Внутренний класс Itr реализует интерфейс Iterator.

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }
}

то естьnew Itr()Когда expectModCount присваивается modCount, а modCount является переменной-членом List, указывающей, сколько раз коллекция была изменена. Так как список выполнил метод добавления 3 раза раньше.

  • Метод add вызывает метод sureCapacityInternal.
  • Метод sureCapacityInternal вызывает метод sureExplicitCapacity.
  • метод sureExplicitCapacity будет выполнятьсяmodCount++

Таким образом, значение modCount равно 3 после трехкратного добавления, поэтомуnew Itr()После этого значение ожидаемого ModCount также равно 3.

При выполнении первого цикла обнаруживается, что "Silent King II" равен str, поэтому выполняемlist.remove(str).

  • Метод удаления вызывает метод fastRemove.
  • Метод fastRemove будет выполнятьсяmodCount++
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
}

Значение modCount становится равным 4.

При выполнении второго цикла выполняется следующий метод Itr (String str = (String) var3.next();), следующий метод вызоветcheckForComodificationметод, когда ожидаемыйModCount равен 3, а modCount равен 4, он должен генерировать ConcurrentModificationException.

На самом деле, это также упоминается в руководстве по разработке Java для Alibaba, не выполняйте операции удаления/добавления элемента в цикле for-each. Используйте метод Iterator для удаления элементов.

Причина на самом деле в том, что мы проанализировали выше, из-за отказоустойчивого механизма защиты.

Как правильно удалять элементы?

1) сломать после удаления

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");

for (String str : list) {
	if ("沉默王二".equals(str)) {
		list.remove(str);
		break;
	}
}

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

Однако, когда в списке есть повторяющиеся элементы, которые нужно удалить, разрыв не подходит.

2) для петли

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");
for (int i = 0, n = list.size(); i < n; i++) {
	String str = list.get(i);
	if ("沉默王二".equals(str)) {
		list.remove(str);
	}
}

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

В первом цикле i равно 0,list.size()равно 3, когда выполняется метод удаления, i равно 1,list.size()Но становится 2, потому что после удаления изменился размер списка, а это значит, что элемент "Silent King Three" был пропущен. Ты понимаешь?

перед удалениемlist.get(1)для «Silk King Three», но после удаленияlist.get(1)стал "о каком интересном программисте писать", иlist.get(0)Станьте «Безмолвным Королем Тройки».

3) Используйте итератор

List<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("一个文章真特么有趣的程序员");

Iterator<String> itr = list.iterator();

while (itr.hasNext()) {
	String str = itr.next();
	if ("沉默王二".equals(str)) {
		itr.remove();
	}
}

Зачем использовать метод удаления Iterator, чтобы избежать отказоустойчивого механизма защиты? Взгляните на исходный код удаления, чтобы понять.

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

Он будет выполнен после удаленияexpectedModCount = modCount, что гарантирует синхронизацию ожидаемого ModCount с modCount.


Если коротко, отказоустойчивость — это механизм защиты, который можно проверить, удалив элементы коллекции в цикле for-each.

То есть for-each по сути является синтаксическим сахаром, который очень полезен при переборе коллекции, но не подходит для манипулирования элементами (добавления и удаления) в коллекции.

Это 63-я часть рубрики "Путь к продвинутым Java-программистам". Расширенный путь программистов Java, юмористический, простой для понимания, чрезвычайно дружелюбный и удобный для начинающих Java😘, включая, помимо прочего, синтаксис Java, структуру сбора Java, Java IO, параллельное программирование Java, виртуальную машину Java и другие основные знания.

GitHub.com/IT Ван Эр/до…

Ярко-белые и темные PDF-файлы также готовы, давайте вместе станем лучшими Java-инженерами, вперед!