1、概述:
foreach是用来循环遍历的方式之一,在java8中新增加的for循环的简化版,虽然说是简化版,并不是说比for或者iterator好用;
主要区别在于:
(1)fori是通过下标访问;
(2)foreach是通过容器的itrator的next()方法来迭代;
这篇文章主要来介绍foreach。
2、foreach样例展示
举例代码:
/**
* @ClassName TestForeach
* @Description TODO
* @Author zhaoyan
* @Date 2019/7/17 16:40
* @Version 1.0
**/
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class TestForeach {
List<String> list = new ArrayList<String>();
public TestForeach() {
this.list.add("北京");
this.list.add("上海");
this.list.add("广州");
this.list.add("深圳");
}
public static void main(String[] args) {
// ArraySplit();
// IteratorSplit();
new TestForeach().foreachSplit();
}
public void foreachSplit() {
for (String s : list) {
System.out.println(s);
}
}
}
运行结果如下:
北京
上海
广州
上述代码定义ArrayList集合,循环遍历输出。
执行代码,生成class文件,并进行反编译得到的代码(我是用IDEA,在IDEA的OUT目录下找到class文件,直接点开即可):
22.png
代码如下:
List<String> list = new ArrayList();
list.add("北京");
list.add("上海");
list.add("广州");
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
String s = (String)iterator.next();
System.out.println(s);
}
反编译可以知道,foreach底层是利用迭代器实现的,初始化获得迭代器,判断条件hasNext(),然后获得next(),最后打印这个结果。
另外foreach不支持在循环中添加删除操作,因为在使用foreach循环的时候数组(集合)就已经被锁定不能被修改,否则会报出java.util.ConcurrentModificationException异常;
但是数组的操作中Iterator是可以进行数组的操作的,这点怎么解释呢?
下面来说明一下这点!!!
3、foreach的remove
上车,飞一波,粘贴代码!!!
public static void foreachSplit() {
List<String> list = new ArrayList<String>();
list.add("北京");
list.add("上海");
list.add("广州");
list.add("深圳");
for (String s : list) {
System.out.println(s);
if ("北京".equals(s)) {
list.remove(s);
}
}
}
运行结果如下:
北京
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at com.zhaoyan.foreach.TestForeach.foreachSplit(TestForeach.java:47)
at com.zhaoyan.foreach.TestForeach.main(TestForeach.java:19)
说明在迭代器使用过程中,禁止修改迭代器中的内容。否侧会抛出java.util.ConcurrentModificationException。
iterator创建的时候modCount被赋值给了expectedModCount,但是调用list的add和remove方法的时候不会同时自动增减expectedModCount,这样就导致两个count不相等,从而抛出异常。
4、重头戏,源码解析
反编译可以知道,foreach底层是利用迭代器实现的,因此:
Iterator var1 = list.iterator();
package java.util
/**
* Returns an iterator over the elements in this list in proper sequence.
*
* <p>The returned iterator is <a href="#fail-fast"><i>fail-fast</i></a>.
*
* @return an iterator over the elements in this list in proper sequence
*/
public Iterator<E> iterator() {
return new Itr();
}
iterator 这个方法在ArrayList和AbstractList中都存在,但AbstractList是抽象类,是被继承之后,要拥有具体的实现方法,抽象类的方法是一定要进行实现的,具体区分关注我之前写的抽象类的博客:https://blog.csdn.net/ITzhaoyan/article/details/94599218
/**
* 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;
cursor:要返回的下一个元素的索引,下一个遍历到的元素的下标,目前还没有开始遍历,所以cursor是0
lastRet :上一次操作的元素的下标,初始值为-1。
expectedModCount :表示迭代器对集合进行修改的次数,是实际上应该轮询的次数;
modCount:modCount是ArrayList的父类AbstractList的一个字段,这个字段的含义是list结构发生变更的次数,通常是add或remove等导致元素数量变更的会触发modCount++。
iterator.hasNext()
public boolean hasNext() {
return cursor != size;
}
size:是集合的大小;
指针的下一个元素还有的话,逻辑为真,没有元素时,循环条件为假,退出循环。
用while进行循环,hasnext作为退出条件进行轮询集合;然后执行下面代码:
String s = (String)iterator.next();
@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 修饰的方法,不能被重写覆盖。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
在调用next()方法的时候,先执行checkForComodification()方法,方法中判断
modCount != expectedModCount,在上述代码中能够肯定,当进行remove操作的时候,
会报java.util.ConcurrentModificationException,就是这段代码的作用。
迭代器内部的每次遍历都会记录List内部的modcount当做预期值,然后在每次循环中用预期值与List的成员变量modCount作比较,但是普通list.remove调用的是List的remove,这时modcount++,但是iterator内记录的预期值=并没有变化,所以会报错。
然后执行获取该下标的数据,并移动元素下标,结果数据进行返回。
5、foreach remove的坑
public static void foreachSplit() {
List<String> list = new ArrayList<String>();
list.add("北京");
list.add("上海");
list.add("广州");
list.add("深圳");
for (String s : list) {
System.out.println(s);
if ("广州".equals(s)) {
list.remove(s);
}
}
}
结果如下:
北京
上海
广州
Process finished with exit code 0
哎,什么情况,竟然能够删除成功!!!
这就是foreach remove的坑。
删除倒数第二个元素的时候,cursor指向最后一个元素的,而此时删掉了倒数第二个元素后,cursor和list.size()正好相等了,所以hasNext()返回false,遍历结束,这样就成功的删除了倒数第二个元素了。这里面的重点是没有执行next()中的checkForComodification()方法,就直接退出了;
6、正确的操作
6.1、fori删除
下面看一段代码:
public void foreachRemove() {
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
list.remove(i);
}
}
这段代码能删除,但是结果如下
北京
广州
Process finished with exit code 0
原来的元素1被remove后,后面的向前拷贝,2到了原来1的位置(下标0),3到了原来2的位置(下标1),size由3变2,i+1=1,输出list.get(1)就成了3,2被漏掉了。同理,每执行一次就会漏掉一个元素;
所以用fori进行元素的删除,我们在操作删除之后,要把下标的位置进行前移,这样就能无遗漏的进行元素删除了。
public void foreachRemove() {
for (int i = 0; i < list.size(); i++) {
System.out.println("删除的元素为:--"+list.get(i));
list.remove(i);
i--;
}
}
执行结果如下:
删除的元素为:--北京
删除的元素为:--上海
删除的元素为:--广州
删除的元素为:--深圳
Process finished with exit code 0
6.2、Iterator迭代器的remove
代码如下:
public void iRemove() {
Iterator<String> itr = list.iterator();
while (itr.hasNext()) {
String s = itr.next();
System.out.println(s);
itr.remove();
}
}
执行结果如下:
北京
上海
广州
深圳
Process finished with exit code 0
迭代器的源码如下:
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();
}
}
看源码了解依然有checkForComodification()校验,但是在remove()之后又重新赋值expectedModCount = modCount;,所以校验是通过的。
7、总结
没啥说的了,以上的分享都是单线程的情况,多线程的情况后续分享,
互相学习,感激每个奋战在一线的开发人员,信息化的进步有你们的贡献!!!
网友评论