ArrayList循环遍历删除元素

list里删除一个元素这个需求是经常遇见的,因为最近要进行代码反讲,正好遇上这样的代码,总结一下处理这个需求的方式和从源码分析下这样处理的原理,例子如下:

import java.util.ArrayList;
public class ArrayListRemove {
    public static void main(String[] args){
        ArrayList<String> list = new ArrayList<String>();
        list.add("a");
        list.add("b");
        list.add("b");
        list.add("c");
        list.add("c");
        list.add("c");
        remove(list);
        for (String s : list) {
            System.out.println("element : " + s);
        }
    }
    public static void remove(ArrayList<String> list) {
        // TODO:
    }
}

写法一:

public static void remove(ArrayList<String> list) {
    for (int i = 0; i < list.size(); i++) {
        String s = list.get(i);
        if (s.equals("b")) {
            list.remove(s);
        }
    }
}

结果:这种最普通的循环写法执行后会发现第二个“b”的字符串没有删掉。
原因:
ArrayList中的remove方法(注意ArrayList中的remove有两个同名方法,只是入参不同,这里看的是入参为Object的remove方法)是怎么实现的:

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;
}

一般情况下程序的执行路径会走到else路径下最终调用faseRemove方法:

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; // Let gc do its work
    }

删除元素时会执行System.arraycopy方法,涉及到数组元素的移动,在遍历第一个字符串b时因为符合删除条件,所以将该元素从数组中删除,并且将后一个元素移动(也就是第二个字符串b)至当前位置,导致下一次循环遍历时后一个字符串b并没有遍历到,所以无法删除。
对这种情况可以倒序删除的方式来避免:

public static void remove(ArrayList<String> list) {
    for (int i = list.size() - 1; i >= 0; i--) {
        String s = list.get(i);
        if (s.equals("b")) {
            list.remove(s);
        }
    }
}

因为数组倒序遍历时即使发生元素删除也不影响后序元素遍历,不涉及元素的移动。

写法二:

public static void remove(ArrayList<String> list) {
    for (String s : list){
        if (s.equals("b")) {
            list.remove(s);
        }
    }
}

结果:这种for-each写法会报出著名的并发修改异常java.util.ConcurrentModificationException。
原因:foreach写法是对实际的Iterable、hasNext、next方法的简写,问题同样处在上文的fastRemove方法中,可以看到第一行把modCount变量的值加一,但在ArrayList返回的迭代器(该代码在其父类AbstractList中):

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

这里返回的是AbstractList类内部的迭代器实现private class Itr implements Iterator,看这个类的next方法:

public E next() {
    checkForComodification();
    try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
    } catch (IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
    }
}

第一行checkForComodification方法:

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

这里会做迭代器内部修改次数检查,因为上面的remove(Object)方法修改了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
}

总结一下:
1)在使用For-Each快速遍历时,ArrayList内部创建了一个内部迭代器iterator,使用的是hasNext和next()方法来判断和取下一个元素。
2)ArrayList里还保存了一个变量modCount,用来记录List修改的次数,而iterator保存了一个expectedModCount来表示期望的修改次数,在每个操作前都会判断两者值是否一样,不一样则会抛出异常;
3)在foreach循环中调用remove()方法后,会走到fastRemove()方法,该方法不是iterator中的方法,而是ArrayList中的方法,在该方法中modCount++; 而iterator中的expectedModCount却并没有改变;
4)再次遍历时,会先调用内部类iteator中的hasNext(),再调用next(),在调用next()方法时,会对modCount和expectedModCount进行比较,此时两者不一致,就抛出了ConcurrentModificationException异常。

附上:
为什么只有在删除倒数第二个元素时程序没有报错的情况呢?
因为在删除倒数第二个位置的元素后,开始遍历最后一个元素时,先会走到内部类iterator的hasNext()方法时,里面返回的是 return cursor != size; 此时cursor是原size()-1,而由于已经删除了一个元素,该方法内的size也是原size()-1,故 return cursor != size;会返回false,直接退出for循环,程序便不会报错。

写法三

public static void remove(ArrayList<String> list) {
    Iterator<String> it = list.iterator();
    while (it.hasNext()) {
        String s = it.next();
        if (s.equals("b")) {
            it.remove();
        }
    }
}

结果:正确删除元素。
原因:这种因为ArrayList与Iterator混合使用时会导致各自的状态出现不一样,最终出现异常。所以Iterator遍历就用Iterator的方法进行删除。
基本上ArrayList采用size属性来维护自已的状态,而Iterator采用cursor来来维护自已的状态。当size出现变化时,cursor并不一定能够得到同步,除非这种变化是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();
    }
}

从上面的代码可以看到当Iterator.remove方法导致ArrayList列表发生变化时,他会更新expectedModCount = modCount来同步这一变化。
但其他方式导致的ArrayList变化,Iterator是无法感知的。ArrayList自然也不会主动通知Iterator们,那将是一个繁重的工作。Iterator到底还是做了努力:为了防止状态不一致可能引发的无法设想的后果,Iterator会经常做checkForComodification检查,以防有变。如果有变,则以异常抛出,所以就出现了上面方法二的异常。

猜你喜欢

转载自blog.csdn.net/qq_16681169/article/details/78241280
今日推荐