深究Java foreach循环的实现原理和其中的坑

深究Java foreach循环的实现原理和其中的坑

文章目录

一、集合遍历的实现(1)for循环(2)使用Iterator(3)foreach循环,JDK 1.5 Syntactic sugar

二、集合中的foreach(1)foreach内部实现原理(1/2-集合)(2)foreach循环的隐藏陷阱

三、数组的 foreach 循环(1)foreach内部实现原理(2/2-数组)

四、你以为 foreach就说完了,效率问题还没说呢!

对一个集合、数组中的元素进行查找、匹配、筛选等都要用到遍历,遍历也是日常必备工具之一。

一、集合遍历的实现

在Java中,集合遍历最常见实现方式主要有3种:

(1)for循环

ArrayList list = new ArrayList<>();

// 简单for循环遍历

for (int i = 0; i < list.size(); i++) {

System.out.println(list.get(i));

}

这是最简单的for循环,借助一个整型变量 i 控制索引的递增,从而实现对集合元素的遍历。

(2)使用Iterator

ArrayList list = new ArrayList<>();

// 借助迭代器进行遍历

Iterator iterator = list.iterator();

while (iterator.hasNext()) {

System.out.println(iterator.next());

}

与for循环写法相比,使用 Iterator 实现遍历虽然代码不如 for 循环简洁,但是 Iterator 有2个优点:

兼容老版本java;在遍历过程中可以iterator.remove()。

如下举例:

ArrayList list = new ArrayList<>();

list.add("A");

list.add("B");

list.add("C");

Iterator iterator = list.iterator();

// 检查list原始size

System.out.println("list's size:" + list.size());

String temp;

while (iterator.hasNext()) {

temp = iterator.next();

if ("A".equals(temp)) {

iterator.remove();

} else {

System.out.println(temp);

}

}

// 检查最新size

System.out.println("new size:" + list.size());

运行结果为:

list's size:3

B

C

new size:2

(3)foreach循环,JDK 1.5 Syntactic sugar

foreach 循环作为语法糖,在JDK1.5版本中首次出现,不仅在语法上更为简洁,可读性也更强:

ArrayList list = new ArrayList<>();

for (String item : list) {

System.out.println(item);

}

Syntactic sugar,语法糖,只是个小甜头,不改变碳水化合物的本质。foreach循环也只是语法层面的技巧,方便开发者使用和阅读,在本质上并没有功能性改进。那么foreach是如何实现循环的呢?

二、集合中的foreach

(1)foreach内部实现原理(1/2-集合)

我们把下边这段代码,进行编译:

public static void main(String[] args) {

ArrayList list = new ArrayList<>();

for (String item : list) {

System.out.println(item);

}

}

编译后,对应.class文件内容为(本次编译环境为 java 8):

public static void main(String[] args) {

ArrayList list = new ArrayList();

Iterator var2 = list.iterator();

while(var2.hasNext()) {

String item = (String)var2.next();

System.out.println(item);

}

}

说明 foreach其实是通过迭代器 Iterator 来实现的——和我们自己实现的Iterator其实是一模一样的。

如果你急切要看foreach内部实现原理(2/2),请往下翻,或直接传送。

(2)foreach循环的隐藏陷阱

这里所说的陷阱,就是不了解foreach的实现原理引发的。如果你把 foreach 当成 for 循环一样使用,然后在遍历的同时使用集合对象的 remove(item),那么恭喜你,即将入坑!

既然对集合框架的 foreach 循环是通过 Iterator 实现的(为什么限定是集合框架的 foreach 循环,请看 foreach内部实现原理(2/2)),那 foreach 必然有迭代器的特性。

Java Collection 中有一种 fail-fast 机制。当多个线程对同一个集合的内容进行操作时,就可能会产生 fail-fast 事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。

而 ConcurrentModificationException 是一种运行时异常,并不会在编译期间被发现。

所以,如果你有这样的代码:

ArrayList list = new ArrayList<>();

list.add("A");

list.add("B");

list.add("C");

for (String item : list) {

// 在foreach中移除集合元素

if ("A".equals(item)) {

list.remove(item);

}

System.out.println(item);

}

编译后生产 .class 文件如下:

ArrayList list = new ArrayList();

list.add("A");

list.add("B");

list.add("C");

String item;

for(Iterator var2 = list.iterator(); var2.hasNext(); System.out.println(item)) {

item = (String)var2.next();

if ("A".equals(item)) {

list.remove(item);

}

}

看起来好像和上边的编译结果不一样,上边是 while(iterator.hasNext()),这个怎么是 for 循环?其实本质是一样的,还是借助于 Iterator:

for 循环 for (a; b; c){} 中 a 为循环初始化语句; b为循环判断条件; c为循环控制语句。所以这个 for 循环中,System.out.println(item) 语句不会对循环产生任何控制,还是以 var2.hasNext() 为循环条件,和之前编译后看到的 while(iterator.hasNext()) 是一样的。

运行结果如下:

Exception in thread "main" java.util.ConcurrentModificationException

at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)

at java.util.ArrayList$Itr.next(ArrayList.java:859)

at Main.main(Main.java:13)

A

所以,Iterator 在工作中是不允许被迭代的对象被改变的。

在遍历集合的同时删除元素的正确的姿势:

// 直接使用 Iterator,不适用 foreach 循环

Iterator iterator = list.iterator();

while (iterator.hasNext()) {

String item = iterator.next();

if ("A".equals(item)) {

/* 这里应该使用 Iterator 实例对象 iterator 的 remove() 方法来移除元

* 素,iterator 会在 remove() 删除当前元素的同时维护索引的一致性;

* 而不能使用集合的实例对象 list 的 remove(item) 方法,否则可能运行时

* 抛 ConcurrentModificationException 异常;

**/

iterator.remove();

}

}

三、数组的 foreach 循环

Java集合的 foreach 实现遍历是通过 Iterator 来实现,因为集合实现了 Iterable 接口:

public interface Collection extends Iterable

然而,你会发现 Java 中数组[ ]也能使用 foreach 循环:

public static void main(String[] args) {

String[] strArray = {"a", "b", "c"};

for (String item : strArray) {

System.out.println(item);

}

}

但是数组可并没有实现 Iterable 接口啊!我嘞个乖乖,弄了这么久,难道我们搞错了?别气馁,因为我相信—— 没有白费的努力,只是有时候不会马上体现出来而已! 那就来证明一下:

(1)foreach内部实现原理(2/2-数组)

我就来看看数组的 foreach 到底发生了什么,打开编译后的 .class 文件:

public static void main(String[] args) {

String[] strArray = new String[]{"a", "b", "c"};

String[] var2 = strArray;

int var3 = strArray.length;

for(int var4 = 0; var4 < var3; ++var4) {

String item = var2[var4];

System.out.println(item);

}

}

原来,数组的 foreach 循环在编译时只是简单地转化成了普通 for 循环而已。索性数组没有 remove(item) 方法,而且数组实例化后长度不可变,也就不会发生类似于上边集合使用 foreach 循环同时删除元素运行时可能报 ConcurrentModificationException 的问题了。

四、你以为 foreach就说完了,效率问题还没说呢!

foreach 循环作用于数组、集合,和 for 循环、Iterator 的效率究竟怎么样呢?不同的场景下怎么选用那种实现更合高效呢?其实我也是看前辈的文章才发现这个问题的 ?:

在 JDK1.7 的 RandomAccess 接口的注释中有这么一段说明:

… As a rule of thumb, a List implementation should implement this interface if, for typical instances of the class, this loop:

for (int i=0, n=list.size(); i < n; i++)

list.get(i);

runs faster than this loop:

for (Iterator i=list.iterator(); i.hasNext(); )

i.next();

就是说:实际经验表明,实现RandomAccess 接口的类实例,假如是随机访问的,使用普通for循环效率将高于使用foreach循环;反过来,如果是顺序访问的,则使用Iterator会效率更高。 那么,哪些类实现可这个接口呢:

All Known Implementing Classes: ArrayList, AttributeList, CopyOnWriteArrayList, RoleList, RoleUnresolvedList, Stack, Vector

其实这个我是没有试过,就当抛砖引玉吧,各位同学有兴趣的可以验证一下,加深理解

但所为辅证,从 ArrayList 源码可以看到:ArrayList 底层是采用数组实现的,如果采用 Iterator 遍历,那么还要创建许多指针去执行这些值(比如next();hasNext())等,这样必然会增加内存开销以及执行效率。

好了,关于 foreach 目前想到的就这些了,以后发现新问题再回来补充。 水平有限,欢迎各位批评吐槽。