集合中的 for-Each循环
数组的加强型的for-Each循环很简单,我们再来看一下集合中的for-Each 循环又是怎么样的。我们都知道集合中的遍历都是通过迭代(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("aa");
list.add("bb");
for(String item:list)
{
if("bb".equals(item))
list.add("cc");
}
}
我们看一下上面例子的 反编译代码:
public static void main(String args[])
{
List list = new LinkedList();
list.add("aa");
list.add("bb");
for(Iterator iterator = list.iterator(); iterator.hasNext();)
{
String item = (String)iterator.next();
if("bb".equals(item))
list.add("cc");
}
}
与数组类似,编译器最终也就是将集合中的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("aa");
list.add("bb");
for (String item : list) {//for-Each
if ("bb".equals(item)) {
list.add("cc"); //直接操作list
}
}
}
运行抛出异常:
上面仅仅是 单线程 下的情况,如果你有并发编程的基础的话,就会知道:在 多线程 的环境中,线程是交替运行的(时间片轮转调度)。这就意味着,如果有两个线程A、B,线程A对集合使用Iterator迭代遍历,线程B则对集合进行增删操作。线程A、B一旦交替运行,就会出现在迭代的同时对集合增删的效果,也会抛出异常。解决办法就是加锁变成原子操作,多线程在这里不是本文重点,不多说了。
2. 集合中的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()方法中更新了一次。
集合中的 for-Each循环的更多相关文章
- (BUG记录)使用迭代器安全的删除处于循环下集合中的元素
今日在写一个功能时,需要从MQ拿取数据集合调用对端系统进行批量处理,为了幂等支持,在循环内部如果不满足调用条件就直接从集合中移除. 以上是一个典型的循环集合内删除的场景任务,工作一年第一次遇到这个场景 ...
- C#不允许在foreach循环中改变数组或集合中元素的值(注:成员的值不受影响)
C#不允许在foreach循环中改变数组或集合中元素的值(注:成员的值不受影响),如以下代码将无法通过编译. foreach (int x in myArray) { x++; //错误代码,因为改变 ...
- Day_11【集合】扩展案例2_使用普通for循环获取集合中索引为3的元素并打印,统计集合中包含字符串"def"的数量,删除集合中的所有字符串",将集合中每个元素中的小写字母变成大写字母def",
分析以下需求,并用代码实现 1.定义ArrayList集合,存入多个字符串"abc" "def" "efg" "def" ...
- 集合中list、ArrayList、LinkedList、Vector的区别、Collection接口的共性方法以及数据结构的总结
List (链表|线性表) 特点: 接口,可存放重复元素,元素存取是有序的,允许在指定位置插入元素,并通过索引来访问元素 1.创建一个用指定可视行数初始化的新滚动列表.默认情况下,不允许进行多项选择. ...
- Iterator的remove方法可保证从源集合中安全地删除对象(转)
如果对正在被迭代的集合进行结构上的改变(即对该集合使用add.remove或clear方法),那么迭代器就不再合法(并且在其后使用该迭代器将会有ConcurrentModificationExcept ...
- 用LINQ在集合中查询特定对象
这里是原文出处: 简单的概括LINQ LINQ是Language-Integrated Query的缩写,是C# 3.0和VB 9.0中新加入的语言特性,可以在编程时使用内置的查询语言进行基于集合的操 ...
- PHP的排列组合问题 分别从每一个集合中取出一个元素进行组合,问有多少种组合?
首先说明这是一个数学的排列组合问题C(m,n) = m!/(n!*(m-n)!) 比如:有集合('粉色','红色','蓝色','黑色'),('38码','39码','40码'),('大号','中号') ...
- 如何删除JAVA集合中的元素
经常我们要删除集合中的某些元素.有些可能会这么写. public void operate(List list){ for (Iterator it = list.iterator(); it.has ...
- Java中的增强 for 循环 foreach
foreach 是 Java 中的一种语法糖,几乎每一种语言都有一些这样的语法糖来方便程序员进行开发,编译期间以特定的字节码或特定的方式来对这些语法进行处理.能够提高性能,并减少代码出错的几率.在 J ...
随机推荐
- Contiki 2.7 Makefile 文件(三)
2.第二部分 这里的usage,targets,savetarget,savedefines都是伪目标. 和all不同,这些伪目标不会被执行,除非显式指定这些目标. 这里有两个目标savetarget ...
- Zeppelin- Linux下安装Zeppelin
前期部署: 下载,解压,配置PATH环境(编辑/etc/profile文件,记得source一下该文件) zepplin配置参考文档:https://zeppelin.apache.org/docs/ ...
- ps炫光素材
炫光闪电笔刷,炫光闪电笔刷,雷电笔刷,自然闪电Photoshop笔刷下载ps炫光素材 素材下载:http://www.huiyi8.com/sc/8695.html
- Linux_服务器_06_VMware虚拟机下安装CentOS7.0图文教程
二.参考资料 1.VMware虚拟机下安装CentOS7.0图文教程
- Jmeter-聚合报告
线程组右键--添加--监听器--聚合报告 Aggreagete Report:jmeter最常用的一个Listener,“聚合报告”. Label:每个jmeter的element(例如HTTP Re ...
- codeforces 569C C. Primes or Palindromes?(素数筛+dp)
题目链接: C. Primes or Palindromes? time limit per test 3 seconds memory limit per test 256 megabytes in ...
- bzoj 2216: Lightning Conductor 单调队列优化dp
题目大意 已知一个长度为\(n\)的序列\(a_1,a_2,...,a_n\)对于每个\(1\leq i\leq n\),找到最小的非负整数\(p\)满足: 对于任意的\(j\), \(a_j \le ...
- css3 实现loading效果
1 圆点渐入渐出 要点: 缩放和透明度的变化,循环变化 <div class="demo1"></div> .demo1{ width: 40px; hei ...
- MySQL 学习三 关于转义
DB2 LIKE谓词查询语句中支持 百分号(%).下划线(_)的使用,不支持方括号([])(注:它会把方括号当成实际的值而非通配符),当我们需要在LIKE 查询条件中将百分号(%).下划线(_)作为实 ...
- Java基础知识之常见关键字(1)
static 特点: 随着类的加载而加载 优先于对象存在 被所有对象所共享 可以直接被类名调用 注意点: 静态方法只能访问静态方法 但是非静态成员可以直接访问静态成员 静态方法中不可以使用this , ...