关于java 数组 forEach() 以及 remove 的使用

来源:互联网 发布:李国杰 大数据 编辑:程序博客网 时间:2024/06/05 13:22

首先,对于Java中数组的遍历,目前有三种方式,for循环、Iterator迭代和forEach遍历,下面就来说说这几种循环方式。由于本文只关心数组,所以下面都以ArrayList为例(基于 jdk 1.7 )。

for循环

这个就比较常见,也是初学程序上手的的循环方式,甚至,我记得,高中数学3 中都还有这种循环技法,如:

        ArrayList<Integer> arrayList=new ArrayList<Integer>();        for (int i=0;i<arrayList.size();i++){            //do something        }

这种方式更像是遍历其索引一样,先拿到索引,然后再取值操作,这种遍历方式也是其中最快的一个,我在
《Java List浅谈(基于jdk1.7)》 底部也做了详细的测试,这里不再赘述,以索引为基础的增删改查都不会是问题,不过索引的范围需要控制好,删除操作时要小心索引的越界问题。

forEach遍历

普通for循环在遍历集合时使用下标来定位集合中的元素,java在jdk1.5中开始支持foreach循环,foreach在一定程度上简化了对集合的遍历,但是foreach不能完全代替for循环。使用方式如下:

        ArrayList<Integer> arrayList=new ArrayList<Integer>();        for (Integer i:arrayList){            // do something        }

限制场景:

  1. 使用foreach来遍历集合时,集合必须实现Iterator接口,foreach就是使用Iterator接口来实现对集合的遍历的
  2. 在用foreach循环遍历一个集合时不能向集合中增加元素,不能从集合中删除元素,否则会抛出ConcurrentModificationException异常。

当使用此方法删除一个元素时候,例如:

    public static void main(String[] strings){        List<Integer> list=new ArrayList<Integer>();        list.add(1);        list.add(2);        list.add(3);        list.add(4);        list.add(5);        for (Integer i:list){            if (i==3)                list.remove(i);        }        System.out.println(list.toString());    }

那么此时就会抛出一个 java.util.ConcurrentModificationException 的异常,其实只要涉及到增加,删除,修改,都会有这个一个异常。

当使用forEach遍历时,编译器会把它转换为 Iterator 迭代器的方式来执行,我们都知道,使用Iterator来remove是没问题的,那为什么 forEach 会有问题呢?不是都已经转换成 Iterator 了吗? 是的,遍历方式是转换为 Iterator 了,可,remove() 却不是 Iterator 接口的方法,仍然用的是 List 的方法。具体原因还是在下面说吧。

Iterator 迭代

曾经很长的一段时间,我都不懂 java.util.List 中 iterator() 方法是干什么用的,获取一个 Iterator 又能怎样?遍历吗?用 for () 就可以啊,干嘛还要用迭代器呢,不是多此一举嘛,直到有一天我看了《Head First 设计模式》这本书,才发现原来这玩意儿竟然还是一种模式。

迭代器模式(Iterator),提供一种方法顺序访问一个聚合对象中的各种元素,而又不暴露该对象的内部表示。

使用迭代器的好处在于,每个集合都知道如何创建自己的Iterator。只要调用 java.util.Collection 的实现类上的 iterator() 方法,就可以返回一个具体的Iterator,而不需要知道或者关心到底使用了哪个具体类,管它是 ArrayList 还是 LinkedList 还是 HashSet,我只需要使用 Iterator 接口就行了。

ArrayList 中实现 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;        public boolean hasNext() {            return cursor != size;        }        @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];        }        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();            }        }        final void checkForComodification() {            if (modCount != expectedModCount)                throw new ConcurrentModificationException();        }    }

Iterator 常用的方法就是 hasNext() 、next() 和 remove() 三个,其中 hasNext() 中,判定是否有下一项的依据是下个数组下标是否等于 size ,如果不等就是还有下一项,感觉怪怪的。好,,先来说说,为什么使用 Iterator 的 remove 就没问题,先不管任何逻辑,单单去想,Iterator 自己的一套东西,总应该是闭环的吧,从头到尾都是用的Iterator的方法,怎么可能会有问题,要是还有问题的话,那就是实现这个接口的类有问题了。

那先说说使用 forEach 遍历然后删除为什么会有问题呢,使用forEach来遍历的时候,编译器会把这种遍历方式编译为 Iterator 的遍历方式,只是遍历方式哦,也就是说只是使用了 Iterator 的 hasNext() 和 next() 方法,可,你却使用ArrayList 的remove方式,并没有用 Iterator 的 remove方法,那人家 Iterator 还要不要面子的啊,这就好像你想要坐飞机去北京,可却买的火车票一样,让你登机才怪了。。。

言归正传,在类似ArrayList 的这种数组结构的数据集合中,一般都会有一个 modCount,字面意思是“修改次数”,实际意思也是修改次数,就是在增删改的时候,使 modCount 加1。而这些操作一般出现在非线程安全的地方,当一个线程在遍历这个集合的时候如果有另外一个线程修改了集合数据,那么这个遍历集合的操作就会抛出ConcurrentModificationException 的异常,以此简单的保证数据的一致性,这也就是所谓fail-fast策略。

卧槽,赶紧回到正题,正题。当在用forEach 遍历的时候,这时候,你其实是在用 Iterator 的 hasNext() 和 next() 方法,正遍历的时候,你用 ArrayList 的remove 方法删掉了一个元素,好吧,那这个时候 modCount 就加了1,好,然后接着 hasNext() ,这也是个关键点,一般返回TRUE,OK,然后就是走的 next() 方法了,这个方法里面,第一句就是 checkForComodification() 检查 modCount 和 它自己维护的 expectedModCount 是否相等,你刚才modCount 加了1,可是 expectedModCount 还是之前的那个值啊,不相等,就异常了呗。那这里也可以看出使用 Iterator 为什么没问题,因为当 modCount 改变的时候,它自己维护的 expectedModCount 也随之改变了呀。是不是很自私???

还有一种情况,请看下面代码:

    public static void main(String[] strings){        List<Integer> list=new ArrayList<Integer>();        list.add(1);        list.add(2);        list.add(3);        list.add(4);        list.add(5);        for (Integer i:list){            if (i==4)                list.remove(i);        }        System.out.println(list.toString());    }

会不会有问题?会不会?是不是和上一个例子一样?是一样的,只是把删掉第三个元素改为了删掉第四个元素了,那有什么区别吗?当然有,这个就不会抛异常。

因为,这不是删除第四个元素,而是,删除倒数第二个元素,倒数第二个。那倒数第二个有什么特别的吗?来想一下,当遍历到了倒数第二个然后删除以后,想一下 hasNext() 方法,本来的 size 是不是现在变成 size-1 了,然后,下一个下标 cursor 和 size 是不是就相等了,然后 hasNext() 就返回 false 了,遍历也就结束了。有问题吗?没报错啊,,嗯,是的,是没抛异常,可是,最后的那个元素也没有遍历到。如果最后那个元素正好是目标元素呢?那就取不到了。

所以,当遍历那些非线程安全的数据结构时,还是尽量使用迭代器吧。

原创粉丝点击