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


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

一、集合遍历的实现

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

(1)for循环

    ArrayList<String> list = new ArrayList<>();

    // 简单for循环遍历
    for (int i = 0; i < list.size(); i++) {
        System.out.println(list.get(i));
    }

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

(2)使用Iterator

	ArrayList<String> list = new ArrayList<>();

	// 借助迭代器进行遍历
	Iterator<String> iterator = list.iterator();
		while (iterator.hasNext()) {
		System.out.println(iterator.next());
	}

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

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

如下举例:

	ArrayList<String> list = new ArrayList<>();
	list.add("A");
	list.add("B");
	list.add("C");

	Iterator<String> 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<String> 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<String> list = new ArrayList<>();

		for (String item : list) {
			System.out.println(item);
		}
	}

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

	public static void main(String[] args) {
		ArrayList<String> 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<String> 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<String> 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<Student> 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<E> extends Iterable<E>

然而,你会发现 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 目前想到的就这些了,以后发现新问题再回来补充。
水平有限,欢迎各位批评吐槽。

  • 15
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值