Java修改ArrayList的常见异常

Java修改ArrayList的常见异常

太长懒得看:对ArrayList进行遍历和修改,要么都用Iterator,要么都不用Iterator。如果非要一边用Iterator遍历,一边不用Iterator修改,请用CopyOnWriteArrayList

开篇首先看一段有问题的代码:

/**
     * 修改数组(添加或者删除)中的元素,此处以删除数组为例。
     * 
     * @param array
     *            要修改的数组
     */
    private ArrayList<String> modifyArrayError0(ArrayList<String> arrayList) {
        if (arrayList != null) {
            // 删除数组中为3的元素
            for (String value : arrayList) {
                if (value.equals("3")) {
                    arrayList.remove(value);
                }
            }
        }
        System.out.println("success");
        return arrayList;
    }

上面的代码是有问题的,执行会遇到java.util.ConcurrentModificationException的错误。我想会有不少人看不出到底哪里出错。那么,我们可以先把上述代码等价变换一下:

private ArrayList<String> modifyArrayError1(ArrayList<String> arrayList){
        if(arrayList != null){
            //删除数组中为3的元素
            Iterator<String> iterator = arrayList.iterator();
            while(iterator.hasNext()){
                String value = iterator.next();
                if(value.equals("3")){
                    arrayList.remove(value);
                }
            }
        }
        return arrayList;
    }

modifyArrayError1与modifyArrayError0是等价的,在java的“foreach”语句中,其实就是利用了Collection的Iterator进行自动遍历,不需要手动改变序号i的数值了。但是这样,还是有很多人不明白为啥报异常。
既然我们什么都不了解,那么直接去看源码就是一条思路,“源码面前,了无秘密”。
根据报错信息:at java.util.ArrayList$Itr.next(ArrayList.java:851),找到如下的源码:

public class ArrayList<E>{
.....
.....
    protected transient int modCount = 0;
.....
/**
     * An optimized version of AbstractList.Itr
     */
    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;

        @SuppressWarnings("unchecked")
        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];
        }

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

在执行到Iterator.next()时,会调用checkForComodification检查modCount和expectedModCount是否相等,如果不相等的话就抛出了ConcurrentModificationException。
modCount是ArrayList内部记录修改次数的字段,每次对ArrayList的修改都会导致modCount加一,而当调用ArrayList.iterator()返回迭代器的时候,会new一个Itr,此时初始化expectedModCount = modCount。既然抛出异常,就说明两者已经不相等,那么我们就去找修改两者值的地方。
在Iterator类中两者是一直相等的,那么只有到ArrayList去找了。每当调用ArrayList的add或者remove的时候,都会对modCount++,而此时并没有对expectedModCount++,于是两者就不相等了。
由以上的分析,我们分析一下我们抛出错误的代码,代码对ArrayList遍历都是通过Iterator,而删除ArrayList的元素是通过ArrayList的remove方法,所以会使expectedModCount与modCount不相等,抛出异常。
既然通过ArrayList删除会导致两者不相等,那么可以看一下Iterator有没有方法可以删除,正好,Iterator有一个remove可以用。所以,解决方法要么就是用Iterator遍历并通过Iterator删除,要么就是不用Iterator遍历不用Iterator删除。

/**
     * 通过Iterator遍历并通过Iterator删除
     * @param arrayList 要修改的ArrayList
     * @return 修改后的ArrayList
     */
private ArrayList<String> modifyArrayCorrect0(ArrayList<String> arrayList){
        if(arrayList != null){
            //删除数组中为3的元素
            Iterator<String> iterator = arrayList.iterator();
            while(iterator.hasNext()){
                String value = iterator.next();
                if(value.equals("3")){
                    iterator.remove();
                }
            }
        }
        return arrayList;
    }




/**
     * 不通过Iterator遍历和修改
     * @param arrayList 要修改的ArrayList
     * @return 修改后的ArrayList
     */
    private ArrayList<String> modifyArrayCorrect1(ArrayList<String> arrayList){
        if(arrayList != null){
            //删除数组中为3的元素
            for(int i = 0; i < arrayList.size(); ++i){
                if(arrayList.get(i).equals("3")){
                    arrayList.remove(i);
                    //删除了一个元素,需要改变一下序号
                    i--;
                }
            }
        }
        return arrayList;
    }

但是总有些不撞南墙不回头的人会问:我就想用Iterator遍历用ArrayList删除整么办?幸好,还有CopyOnWriteArrayList来拯救:

/**
     * 通过CopyonWriteArrayList解决异常为题
     * @param arrayList 要修改的ArrayList
     * @return 修改后的ArrayList
     */
    private CopyOnWriteArrayList<String> modifyArrayCorrect2(CopyOnWriteArrayList<String> arrayList){
        if(arrayList != null){          
            //删除数组中为3的元素
            Iterator<String> iterator = arrayList.iterator();
            while(iterator.hasNext()){
                String value = iterator.next();
                if(value.equals("3")){
                    arrayList.remove(value);
                }
            }
        }
        return arrayList;
    } 

CopyOnWriteArrayList何许人也?本领整么会如此强大?那可说来话长了。。。
既然时间都贵如油,我就长话短说了:名字就能反映其功能,在写操作时复制一份数据出来。CopyOnWriteArrayList在add或者remove时,都会把所有数据拷贝出一份,然后在副本上添加或者删除元素,当完成操作后,就会把用复制品替代“元身”,以后就直接用复制的那份数据了。此过程是线程安全的,不会抛出ConcurrentModificationException。在完成操作前,你通过get获取到的数据还是以前的“元身”,只有当完成操作后去读才能获得最新的。

public class CopyOnWriteArrayList<E>{
    private E get(Object[] a, int index) {
        return (E) a[index];
    }
/**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

    public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }

    static final class COWIterator<E> implements ListIterator<E> 
    {
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }
        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; {@code remove}
         *         is not supported by this iterator.
         */
        public void remove() {
            throw new UnsupportedOperationException();
        }
        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; {@code add}
         *         is not supported by this iterator.
         */
        public void add(E e) {
            throw new UnsupportedOperationException();
        }

    }
}

可以看到CopyOnWriteArrayList的add函数中是通过Arrays.copyOf拷贝出一份新的数据,然后将要add的元素e添加到新的数据里,最后用新的数据替换就的数据,整个过程是加锁的。其Iterator就不支持remove和add操作,所以CopyOnWriteArrayList可以避免抛出ConcurrentModificationException,但是缺点也很明显,每次都会把所有数据拷贝一份,牺牲了性能获取了线程安全,到底怎样取舍需要根据具体情况来确定。

后记:后来在看安卓面试常见问题的时候,发现此问题是有专业术语的:

1、fail-fast:在遍历一个集合时,当集合结构被修改,会抛出Concurrent Modification Exception
2、fail-safe:任何对集合结构的修改都会在一个复制的集合上进行修改,因此不会抛出ConcurrentModificationException

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值