引言:为什么for-each成为Java中的“循环之王”?
\n\n如果您至少写过一周的Java代码,您一定遇到过for (Type var : collection)这种结构。这种简洁的语法被称为for-each循环(或增强型for循环)。它早在Java 5(2004年)中就出现了,并迅速成为开发者的最爱。
但是,您是否知道当JVM遇到这种结构时,底层发生了什么?为什么for-each对于数组来说可能比普通for循环慢?为什么即使您没有删除任何内容,它有时也会抛出ConcurrentModificationException?
在本文中,我们将从字节码层面深入分析Java中for-each的工作原理,与经典循环进行比较,并学习如何避免常见的陷阱。让我们开始吧!
\n\n语法与初步了解
\n\n基本语法如下:
\n\nfor (元素类型 变量名 : 集合或数组) {\n // 循环体\n}\n\n使用ArrayList的示例:
List<String> names = Arrays.asList("安娜", "鲍里斯", "维克多");\n\nfor (String name : names) {\n System.out.println("你好, " + name + "!");\n}\n\n输出:
\n\n你好, 安娜!\n你好, 鲍里斯!\n你好, 维克多!\n\n很简单,对吧?但在这简单的背后,隐藏着两种完全不同的机制——一种用于数组,另一种用于集合。
\n\nfor-each如何与数组一起工作?
\n\n当您为数组编写for-each时,Java编译器会将其转换为一个带有索引的普通for循环。没有任何魔法。
您的代码:
\n\nint[] numbers = {10, 20, 30};\n\nfor (int num : numbers) {\n System.out.println(num);\n}\n\n编译后(反编译字节码——您会看到):
\n\nint[] numbers = new int[]{10, 20, 30};\nfor (int i = 0; i < numbers.length; i++) {\n int num = numbers[i];\n System.out.println(num);\n}\n\n重要结论:对于数组,for-each和普通for循环在性能上是相同的。没有迭代器的额外开销。
\n\nfor-each如何与集合(List, Set)一起工作?
\n\n这里开始变得有趣了。对于集合,for-each使用迭代器。
\n\n您的代码:
\n\nList<String> list = new ArrayList<>();\nlist.add("Java");\nlist.add("Python");\nlist.add("C++");\n\nfor (String lang : list) {\n System.out.println(lang);\n}\n\n编译器将其转换为:
\n\nList<String> list = new ArrayList<>();\nlist.add("Java");\nlist.add("Python");\nlist.add("C++");\n\nfor (Iterator<String> it = list.iterator(); it.hasNext(); ) {\n String lang = it.next();\n System.out.println(lang);\n}\n\n也就是说:
\n- \n
- 调用
list.iterator()——获取迭代器。 \n - 条件为
it.hasNext()。 \n - 循环体内为
it.next()。 \n - 变量
lang由next()的结果初始化。 \n
这在实践中意味着什么?
\n- \n
- 不能在for-each内部通过
list.remove()删除元素——您会得到ConcurrentModificationException。请使用it.remove()或普通for循环。 \n - 不能在迭代期间修改集合(添加/删除)——同样会抛出该错误。 \n
- 性能:对于
ArrayList,与普通for循环的差异很小(创建迭代器对象的开销)。对于LinkedList——for-each比for (int i = 0; i < list.size(); i++)快得多,因为LinkedList的get(i)每次都是O(n)复杂度! \n
Map呢?遍历键值对
\n\nMap不直接实现Iterable,因此for-each