涨姿势:深入 foreach循环
我们知道集合中的遍历都是通过迭代(iterator)完成的。
也许有人说,不一定非要使用迭代,如:
- List<String> list = new LinkedList<String>();
- list.add("a");
- list.add("b");
- list.add("c");
- for(int i=0;i<list.size();i++){
- String item = list.get(i);
- System.out.println(item);
- }
这种方式对于基于链表实现的List来说,是比较耗性能的。
因为get(int i)方法包含了一个循环,而且这个循环就是迭代遍历一次List,直到遇到第i个元素,才停止循环,返回第i个元素。对于数量小,遍历不频繁的List来说,开销可以忽略。否则,开销将不容忽视。
所以集合遍历是使用迭代器Iterator来遍历的:
- List<String> list = new LinkedList<String>();
- list.add("a");
- list.add("b");
- list.add("c");
- //获取集合的迭代器
- Iterator<String> itor = list.iterator();
- //集合的普通for循环
- for(;itor.hasNext();){//相当于 while(itor.hasNext())
- String item = itor.next();
- System.out.println(item);
- }
对应的for-Each循环的例子:
- List<String> list = new LinkedList<String>();
- list.add("a");
- list.add("b");
- list.add("c");
- for(String item:list){//for-Each
- System.out.println(item);
- }
可以看出,for-Each循环比普通for循环要简洁很多。
我们回答上面的两个问题:
- 编译器是如何处理集合中的for-Each循环的?
- public static void main(String args[])
- {
- List list = new LinkedList();
- list.add("aaa");
- list.add("bbb");
- for(String item:list)
- {
- if("bbb".equals(item))
- list.add("ccc");
- }
- }
- 反编译上面代码:
- public static void main(String args[])
- {
- List list = new LinkedList();
- list.add("aaa");
- list.add("bbb");
- for(Iterator iterator = list.iterator(); iterator.hasNext();)
- {
- String item = (String)iterator.next();
- if("bbb".equals(item))
- list.add("ccc");
- }
- }
与数组类似,编译器最终也就是将集合中的for-Each循环处理成集合的普通for循环。而集合的Collection接口通过扩展Iterable接口来提供iterator()方。
那么我们换一个角度,是不是只要实现 Iterable接口,提供iterator()方法,也可以使用 for-Each循环呢?
例子:
- class MyList<T> implements Iterable<T>{
- private ArrayList<T> list = new ArrayList<>();
- public void addId(T id){
- list.add(id);
- }
- public boolean removeId(T id){
- return list.remove(id);
- }
- @Override
- public Iterator<T> iterator() {//扩展自Iterable接口
- //为了简单起见,就直接使用已有的迭代器
- return list.iterator();
- }
- public static void main(String[] args) {
- MyList<String> myList = new MyList<>();
- myList.addId("666999");
- myList.addId("973219");
- //for-Each
- for(String item:myList){
- System.out.println(item);
- }
- }
- }
编译通过。
所以只要实现了Iterable接口的类,都可以使用for-Each循环来遍历。
集合迭代的陷阱
集合循环遍历时所使用的迭代器Iterator有一个要求:在迭代的过程中,除了使用迭代器(如:Iterator.remove()方法)对集合增删元素外,是不允许直接对集合进行增删操作。否则将会抛出 ConcurrentModificationException异常。
所以,由于集合的for-Each循环本质上使用的还是Iterator来迭代,因此也要注意这个陷阱。for-Each循环很隐蔽地使用了Iterator,导致程序员很容易忽略掉这个细节,所以一定要注意。
看下面的例子,for-Each循环中修改了集合。
- public static void main(String[] args) {
- List<String> list = new LinkedList<>();
- list.add("aaa");
- list.add("bbb");
- for (String item : list) {//for-Each
- if ("bbb".equals(item)) {
- list.add("ccc"); //直接操作list
- }
- }
- }
运行抛出异常.
上面仅仅是单线程下的情况,如果在 多线程 的环境中,线程是交替运行的(时间片轮转调度)。这就意味着,如果有两个线程A、B,线程A对集合使用Iterator迭代遍历,线程B则对集合进行增删操作。线程A、B一旦交替运行,就会出现在迭代的同时对集合增删的效果,也会抛出异常。
解决办法就是加锁变成原子操作。
- 集合中的for-Each循环能代替集合的普通for循环吗?
同样也不能。
集合中的for-Each循环的局限性与数组的for-Each循环是一样的。集合的for-Each循环是不能对集合进行增删操作、也不能获取索引。
而集合的普通for循环可以使用的迭代器提供了对集合的增删方法(如:Iterator.remove,ListIterator.add()),获取索引的方法(如:ListIterator.nextIndex()、ListIterator.previousIndex());
扩展:Iterator源码分析
我们来分析一下Iterator源码,主要看看为什么在集合迭代时,修改集合可能会抛出ConcurrentModificationException异常。以ArrayList中实现的Iterator为例。
先来看一下ArrayList.iterator()方法,如下:
- public Iterator<E> iterator() {
- return new Itr();
- }
iterator()方法直接创建了一个类Itr的对象。那就接着看 Itr类的定义吧!发现Itr其实是ArrayList的内部类,实现了 Iterator 接口。
- /**
- * 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;
- public boolean hasNext() {
- return cursor != size;
- }
- @SuppressWarnings("unchecked")
- public E next() {
- checkForComodification();
- int i = cursor;
- if (i >= size)
- throw new NoSuchElementException();
- //ArrayList的底层数组
- 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
- expectedModCount = modCount;
- } catch (IndexOutOfBoundsException ex) {
- throw new ConcurrentModificationException();
- }
- }
- @Override
- @SuppressWarnings("unchecked")
- public void forEachRemaining(Consumer<? super E> consumer) {
- Objects.requireNonNull(consumer);
- final int size = ArrayList.this.size;
- int i = cursor;
- if (i >= size) {
- return;
- }
- final Object[] elementData = ArrayList.this.elementData;
- if (i >= elementData.length) {
- throw new ConcurrentModificationException();
- }
- while (i != size && modCount == expectedModCount) {
- consumer.accept((E) elementData[i++]);
- }
- // update once at end of iteration to reduce heap write traffic
- cursor = i;
- lastRet = i - 1;
- checkForComodification();
- }
- final void checkForComodification() {
- if (modCount != expectedModCount)
- throw new ConcurrentModificationException();
- }
- }
ArrayList.this.elementData是ArrayList的底层数组,这些方法都是对ArrayList.this.elementData这个底层数组进行操作。
重点看一下checkForComodification()方法,它是用来抛出ConcurrentModificationException异常,判断modCount与expectedModCount是否相等。modCount存储的AarryList中的元素个数。而expectedModCount则是对象创建时将modCount的值赋给它,也就是说expectedModCount存储的是迭代器创建时元素的个数。
那么checkForComodification()方法其实在比较迭代期间,ArrayList元素的个数 是否发生了改变,如果改变了,就抛出异常。
注意,expectedModCount除了在声明时赋值外,也在remove()方法中更新了一次。
涨姿势:深入 foreach循环的更多相关文章
- JAVA中的for-each循环与迭代
在学习java中的collection时注意到,collection层次的根接口Collection实现了Iterable<T>接口(位于java.lang包中),实现这个接口允许对象成为 ...
- foreach循环 Java
第一次遇到foreach循环,是在PHP的数组中,同样,在Java数组中,也遇到了foreach循环,都是用来遍历数组(集合).遍历数组,首先想到的一般都是用while,do while,for循环, ...
- 集合框架遍历方式之——for-each循环
从Java5起,在Java中有了for-each循环,可以用来循环遍历collection和array.Foreach循环允许你在无需保持传统for循环中的索引,或在使用iterator /ListI ...
- Java语法糖1:可变长度参数以及foreach循环原理
语法糖 接下来几篇文章要开启一个Java语法糖系列,所以首先讲讲什么是语法糖.语法糖是一种几乎每种语言或多或少都提供过的一些方便程序员开发代码的语法,它只是编译器实现的一些小把戏罢了,编译期间以特定的 ...
- 巧用array_map()和array_reduce()替代foreach循环
1.array_reduce( $arr , callable $callback ) 使用回调函数迭代地将数组简化为单一的值. 其中$arr 为输入数组,$callback($result , $v ...
- For-Each循环
For-Each循环也叫增强型的for循环,或者叫foreach循环. For-Each循环是JDK5.0的新特性(其他新特性比如泛型.自动装箱等). For-Each循环的加入简化了集合的遍历. 语 ...
- php学习笔记:foreach循环访问关联数组里的值
foreach循环可以将数组里的所有值都访问到,下面我们展示下,用foreach循环访问关联数组里的值. 例如: $fruit=array('apple'=>"苹果",'ba ...
- yii2通过foreach循环遍历在一个用户组中取出id去另一表里查寻信息并且带着信息合并原数组信息---案例
yii2通过foreach循环遍历在一个用户组中取出id去另一表里查寻信息并且带着信息合并元数组信息---案例 public function actionRandomLists(){ //查询到了所 ...
- 关于JSP的C标签之forEach循环分隔符
页面中可能说出现在forEach循环中间需要出力分隔符的问题, 比如: 小明 1年级,小王 2年级, 小张 3年级(循环单位是 ${bean.name} ${bean.class}) 此时的逗号出力, ...
随机推荐
- 浅谈树状数组(为什么lowbit(x)=x&(-x)
树状数组是一种支持单点修改和查询前缀和的数据结构 网上很多讲解它的博客了 这里重点讲一下为什么lowbit(x)=x&(-x) 树状数组代码量相对于线段树基本可以不计(太好写了) 因此NOIp ...
- python if __name__=='__main__'的理解
定义一个模块叫module.py: def main(): print "we are in %s" %__name__ if __name__=='__main__': main ...
- CodeForces - 589A (STL容器的使用)
Polycarp has quite recently learned about email aliases. Of course, he used to suspect that the case ...
- 最短路,floyd算法,图的最短路径
题目描述: 在每年的校赛里,所有进入决赛的同学都会获得一件很漂亮的t-shirt.但是每当我们的工作人员把上百件的衣服从商店运回到赛场的时候,却是非常累的!所以现在他们想要寻找最短的从商店到赛场的路线 ...
- python自学第11天-单线程并发、迭代器,序列化,获取路径
单线程并发 import time def consumer(name): print("%s 准备吃包子了"%name) while True: baozi=yield#变成一个 ...
- c#的默认访问权限
1.命名空间下的元素的默认访问修饰符 public : 同一程序集的其他任何代码或引用该程序集的其他程序集都可以访问该类型或成员. internal : 同一程序集中的任何代码都可以访问该类型或成员, ...
- 入门项目 A5-1 interface-bank 第三方接口1
from db import db_handler # 提现接口 def withdraw_interface(name, money): # 定义提现接口,传入name与money参数 user_d ...
- ubuntu16.04下python2、python3环境选择与python升级(pip版本切换)
参考链接:https://www.jianshu.com/p/63c1f22e1fed Ubuntu中python版本问题: 添加PPA: sudo add-apt-repository ppa:jo ...
- 【linux基础】cuDNN版本查询
参考 1. 查看cudnn版本; 完
- sql server中case when的用法
Case具有两种格式.简单Case函数和Case搜索函数. --简单Case函数 CASE sex WHEN '1' THEN '男' WHEN '2' THEN '女' ELSE '其他' END ...