集合中的 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 ...
随机推荐
- 0CTF 2017 部分Web的某些不一样的思路
洒家参加了0CTF 2017,做了一些题目.赛后过了好几天,看网上已经有了一些写得不错的Writeup,这里就写一写洒家的一些不一样的思路. 一些不错的Writeup https://ctftime. ...
- ctypes模块与pywin32模块
ctypes模块: 主要用于调用c动态链接库. 1.聊聊Python ctypes模块 2.ctypes模块管理 相关网址: pywin32模块: 用于访问win32API函数(win32api模块) ...
- leetcode 111 Minimum Depth of Binary Tree(DFS)
Given a binary tree, find its minimum depth. The minimum depth is the number of nodes along the shor ...
- WebP探索
不同场景下WebP的使用方案总结: App:Android4.0以上提供原生支持,其他版本使用官方提供的解析库(Android.iOS): 浏览器:JS能力检测,对支持 WebP 的用户输出 ...
- python爬虫知识点总结(九)Requests+正则表达式爬取猫眼电影
一.爬取流程 二.代码演示 #-*- coding: UTF-8 -*- #_author:AlexCthon #mail:alexcthon@163.com #date:2018/8/3 impor ...
- 五 akka streams kafka
(转载 https://doc.akka.io/docs/akka-stream-kafka/current/home.html) 一: Akka Streams Kafka, also known ...
- [poj3450]Corporate Identity(后缀数组)
题意:多个字符串的最长公共子串. 解题关键:字符串的任何一个子串都是这个字符串的某个后缀的前缀.求A和B的最长公共子串等价于求A的后缀和B的后缀的最长公共前缀的最大值. 后缀数组的经典例题,连接在一起 ...
- 流媒体中ffmpeg 命令的使用
在linux系统中,使用到的有关流媒体音视频流进行处理的ffmpeg 命令的常用的命令己命令对应的参数如下:记录一下: 1.分离视频音频流 ffmpeg -i input_file -vcodec c ...
- eclipse中jquery.js文件有错误提示…
eclipse中jquery.js文件有错误提示的解决办法 2013-04-06 19:18 浏览次数:382 由于jquery.js文件进行了压缩,压缩之后的语法eclipse无法完全识别,所以有错 ...
- 文件解析库doctotext安装和使用
安装doctotext 1 安装GCC到4.6以上 tar jxf gcc-4.7.0.tar.bz2 cd gcc-4.7.0 编译 ./contrib/download_prerequisites ...