ArrayList遍历删除元素

1,733 阅读2分钟

ArrayList遍历元素的同时进行元素删除操作

1、使用for顺序遍历并i--

public static void main(String[] args) {
    List<String> list = new ArrayList<>(Arrays.asList("0", "11", "22", "22", "33", "22", "23", "55"));
    for (int i = 0; i < list.size(); i++) {
        if ("22".equals(list.get(i))) {
            list.remove(i);
            // 这里需要加上i--,遗漏了被删除元素后的一个元素,譬如连续两个22,第二个会跳过带
            // 来错误结果(只会删除第一个22),并不是会抛异常
            i--;
        }
    }
    System.out.println(list);
}
运行结果:
[0, 11, 33, 23, 55]
  • 如注释那样,假设不使用i--,当数组为["0", "11", "22", "22", "33", "22", "23", "55"]并要删除元素"22"时会造成连续两个"22"中只删除了第一个"22",(list移除一个元素后所有元素前移,移除元素的index进而前移一位,该元素会躲过遍历检测)而带来错误结果[0, 11, 22, 33, 23, 55]

2、for循环倒叙遍历

  • 由于使用倒叙遍历,不会因为list删除某元素后移位而带来相邻的后一个元素的“逃逸事件”
public static void main(String[] args) {
    List<String> list = new ArrayList<>(Arrays.asList("0", "11", "22", "22", "33", "22", "23", "55"));
    for (int i = list.size() - 1; i >= 0; i--) {
        if ("22".equals(list.get(i))) {
            list.remove(i);
        }
    }
    System.out.println(list);
}
运行结果:
[0, 11, 23, 33, 55]

3、使用Iterator.remove删除

  • 使用Iterator遍历后,再使用iterator.remove()方法进行删除即可
public static void main(String[] args) {
    List<String> list = new ArrayList<>(Arrays.asList("0", "11", "22", "22", "33", "22", "23", "55"));
    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        if ("22".equals(iterator.next())) {
            iterator.remove(); // 正确方式
           // list.remove(iterator.next()); // 会导致ConcurrentModificationException异常
        }
    }
    System.out.println(list);
}
运行结果:
[0, 11, 33, 23, 55]
  • 关键点在于iterator.remove(),查看ArrayList父类AbstractList的内部类Itr的remove方法
public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        AbstractList.this.remove(lastRet);
        if (lastRet < cursor)
            cursor--;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException e) {
        throw new ConcurrentModificationException();
    }
}
  • 在iterator.remove方法内部会手动重置expectedModCount = modCount进而避免在next方法中的checkForComodification方法抛出ConcurrentModificationException异常(并发修改异常)

4、使用Java8的removeIf方法

public static void main(String[] args) {
    ArrayList<String> arrayList = new ArrayList<>(Arrays.asList("0", "11", "22", "22", "33", "22", "23", "55"));
    arrayList.removeIf("22"::equals);
    System.out.println(arrayList);
}
运行结果:
[0, 11, 33, 23, 55]
  • arrayList.removeIf源码中借助了BitSet(long数组)类记录要移除的元素的index,然后进行元素内的逐个赋值(使用debug逐步运行更易懂)
@Override
public boolean removeIf(Predicate<? super E> filter) {
    Objects.requireNonNull(filter);
    // figure out which elements are to be removed
    // any exception thrown from the filter predicate at this stage
    // will leave the collection unmodified
    // 要删除的元素的个数
    int removeCount = 0;
    // 使用BitSet存储符合条件的index位置
    final BitSet removeSet = new BitSet(size);
    final int expectedModCount = modCount;
    final int size = this.size;
    // 遍历所有的元素并将符合条件位置的index存储进removeSet
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        @SuppressWarnings("unchecked")
        final E element = (E) elementData[i];
        if (filter.test(element)) {
            removeSet.set(i);
            removeCount++;
        }
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }

    // shift surviving elements left over the spaces left by removed elements
    final boolean anyToRemove = removeCount > 0;
     // 存在需要删除的元素时才继续遍历,否则直接返回false
    if (anyToRemove) {
        final int newSize = size - removeCount; // 无需删除的元素个数
        // 这里进行第二次遍历将要删除元素的位置的值赋值给后面不需要删除元素的值
        for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
         // 遇到删除元素的index就将i赋值为下一个非删除元素的index
            i = removeSet.nextClearBit(i);
            // 重新赋值
            elementData[j] = elementData[i];
        }
        // 删除元素后将后面空余的位置置null
        for (int k=newSize; k < size; k++) {
            elementData[k] = null;  // Let gc do its work
        }
        this.size = newSize;
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
        modCount++;
    }

    return anyToRemove;
}

错误方式1:增强for

public static void main(String[] args) {
    List<String> list = new ArrayList<>(Arrays.asList("0", "11", "22", "23", "33", "22", "55"));
    // 使用增强for本质上也是Iterator
    for (String i : list) {
        if ("22".equals(i)) {
            list.remove(i);
        }
    }
    System.out.println(list);
}
运行抛出ConcurrentModificationException异常
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
	at java.util.ArrayList$Itr.next(ArrayList.java:859)
	at collection.list.RemoveArraylistItem.main(RemoveArraylistItem.java:28)

抛异常原因分析

  • 每一次遍历都会调用父类AbstractList中的内部类Itr的next方法
public E next() {
    // 问题处在这里
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}
  • 方法执行遍历前会去执行checkForComodification方法检测
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
  • 方法内部,只要modCount与expectedModCount不等就会抛出ConcurrentModificationException异常
  • expectedModCount是Itr的成员变量,初始时赋值为modCount
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;
    ...
  • 而arrayList.remove方法会修改modCount的值
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;
}
  • 接着看fastRemove方法
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
}
  • 果然,每执行一次remove(即fastRemove)会导致modCount++从而导致modCount != expectedModCount进而在checkForComodification中抛出ConcurrentModificationException异常

错误方式2:使用forEach

public static void main(String[] args) {
    List<String> list = new ArrayList<>(Arrays.asList("0", "11", "22", "22", "33", "22", "23", "55"));
    // ArrayList的forEach源码中会检测modCount == expectedModCount
    list.forEach(i -> {
        if ("22".equals(i)) {
            list.remove(i);
        }
    });
    System.out.println(list);
}
运行结果:
Exception in thread "main" java.util.ConcurrentModificationException
	at java.util.ArrayList.forEach(ArrayList.java:1260)
	at collection.list.RemoveArraylistItem.main(RemoveArraylistItem.java:29)

抛异常原因分析

  • 查看ArrayList的forEach方法
@Override
public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    @SuppressWarnings("unchecked")
    final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);
    }
    // 这里会进行检测
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
  • 在源码末尾也会进行modCount与expectedModCount的检测