| 特性 | 传统 for 循环 |
for-each 循环 (增强型 for 循环) |
|---|---|---|
| 语法 | for (初始化; 条件; 更新) |
for (元素类型 元素变量 : 遍历对象) |
| 遍历对象 | 数组、Iterable 对象(如 List, Set) |
仅限 Iterable 对象(如 List, Set),不能直接用于数组(Java 5-8),但 Java 9+ 可以。 |
| 索引/下标 | 可以获取,可以自由控制遍历顺序(如倒序、跳步) | 无法直接获取,只能按默认顺序从头到尾遍历一次。 |
| 修改集合 | 安全,不会导致 ConcurrentModificationException |
不安全,如果在遍历过程中使用 remove() 方法会抛出异常。 |
| 适用场景 | 需要索引、需要倒序遍历、需要修改集合内容、性能要求极高(对数组) | 简单、安全地遍历集合或数组中的每一个元素,代码更简洁。 |
| 底层实现 | 显式的索引控制 | 基于 Iterator 实现。 |
详细对比与代码示例
语法与可读性
-
传统
for循环 语法比较冗长,需要手动管理索引变量(如i)。
(图片来源网络,侵删)// 遍历数组 String[] names = {"Alice", "Bob", "Charlie"}; for (int i = 0; i < names.length; i++) { System.out.println(names[i]); } // 遍历 List List<String> nameList = Arrays.asList("Alice", "Bob", "Charlie"); for (int i = 0; i < nameList.size(); i++) { System.out.println(nameList.get(i)); } -
for-each循环 语法非常简洁,意图明确——“对于集合/数组中的每一个元素,执行以下操作”。// 遍历数组 (Java 5+) String[] names = {"Alice", "Bob", "Charlie"}; for (String name : names) { System.out.println(name); } // 遍历 List List<String> nameList = Arrays.asList("Alice", "Bob", "Charlie"); for (String name : nameList) { System.out.println(name); }
在不需要索引的场景下,for-each 的可读性远胜于传统 for 循环。
索引访问与遍历控制
这是两者最核心的区别之一。
-
传统
for循环 优势:可以自由地通过索引访问任何位置的元素,并控制遍历流程。
(图片来源网络,侵删)// 1. 访问特定索引的元素 System.out.println("第一个元素是: " + names[0]); // 2. 倒序遍历 System.out.println("倒序遍历:"); for (int i = names.length - 1; i >= 0; i--) { System.out.println(names[i]); } // 3. 跳步遍历 (只访问偶数索引) System.out.println("跳步遍历:"); for (int i = 0; i < names.length; i += 2) { System.out.println(names[i]); } -
for-each循环 劣势:无法获取当前元素的索引,你只能拿到元素本身,不知道它位于第几个位置,遍历顺序是固定的,不能倒序或跳步。// 你无法在循环体内知道当前是第几个元素 int index = 0; // 需要在外面手动定义一个计数器 for (String name : names) { System.out.println("元素 " + (index++) + " 是: " + name); } // 这种方式虽然可行,但破坏了 `for-each` 循环的简洁性,有时还不如用传统 for 循环。
如果需要索引、倒序或跳步遍历,必须使用传统 for 循环。
修改遍历的集合
这是一个非常容易出错的地方,也是面试官喜欢考察的点。
-
传统
for循环 相对安全,如果你通过list.remove(i)来移除元素,需要注意索引的更新,但不会立即抛出ConcurrentModificationException。
(图片来源网络,侵删)List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); // 正确的删除方式:从后往前删除,或者更新索引 for (int i = 0; i < numbers.size(); i++) { if (numbers.get(i) % 2 == 0) { numbers.remove(i); // 删除后,后续元素会前移,所以索引 i 也应该前移 i--; // 关键:删除后,i--,使得下一次循环时 i 指向新的当前元素 } } System.out.println(numbers); // 输出: [1, 3, 5] -
for-each循环 非常危险。for-each循环的内部实现是基于Iterator的,当你调用list.remove()时,它实际上会调用iterator.remove(),如果你在循环中直接调用集合的remove()方法,而不是iterator.remove(),就会导致集合的修改计数器和迭代器的预期不一致,从而抛出java.util.ConcurrentModificationException异常。List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); try { for (Integer number : numbers) { if (number % 2 == 0) { // 错误!这是在直接修改集合,迭代器不知道这个变化 numbers.remove(number); // 抛出 ConcurrentModificationException } } } catch (ConcurrentModificationException e) { System.out.println("捕获到异常: " + e); }在
for-each中如何安全地删除元素呢? 你不能。for-each循环的设计初衷就是只读遍历,如果你需要在遍历时删除元素,应该使用传统的for循环(并正确处理索引)或者使用Iterator的remove()方法。// 正确的方式:使用 Iterator List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5)); Iterator<Integer> iterator = numbers.iterator(); while (iterator.hasNext()) { Integer number = iterator.next(); if (number % 2 == 0) { iterator.remove(); // 安全地移除元素 } } System.out.println(numbers); // 输出: [1, 3, 5]
如果需要在遍历过程中修改(添加或删除)集合内容,绝对不能使用 for-each 循环,应使用传统 for 循环(谨慎)或 Iterator 的 remove() 方法(推荐)。
性能对比
-
对于
List(如ArrayList)- 传统
for循环:通过list.get(i)访问,时间复杂度为 O(1),性能非常高。 for-each循环:基于Iterator,每次调用next()都会调用list.get(i),性能与传统for循环几乎完全相同,现代 JVM 优化得很好,两者性能差异可以忽略不计。
- 传统
-
对于
LinkedList- 传统
for循环:list.get(i)需要从头或尾开始遍历,时间复杂度为 O(n),在LinkedList中性能极差。 for-each循环:基于Iterator,Iterator内部持有Node的引用,next()方法是 O(1) 操作。在LinkedList上,for-each循环的性能远超传统for循环。
- 传统
-
对于数组
- 传统
for循环:直接通过索引访问,性能最优。 for-each循环:性能与传统for循环几乎相同。
- 传统
在 ArrayList 和数组上,两者性能相当,在 LinkedList 上,for-each 是更好的选择。
何时使用哪个?
这是一个简单的决策指南:
| 你的需求 | 推荐的循环类型 | 原因 |
|---|---|---|
| 只需要简单地读取集合/数组中的每一个元素 | for-each |
代码最简洁、最安全、可读性最高。 |
| 需要元素的索引 | 传统 for |
for-each 无法提供索引。 |
| 需要倒序遍历或跳步遍历 | 传统 for |
for-each 无法控制遍历顺序。 |
| 需要在遍历时删除集合中的元素 | Iterator 的 remove() 方法 |
for-each 会抛出异常,传统 for 容易出错。 |
| 需要在遍历时向集合中添加元素 | 传统 for |
for-each 同样会抛出异常,但添加逻辑更复杂,传统 for 更可控。 |
遍历的是 LinkedList |
for-each |
性能远优于传统 for 循环。 |
遍历的是 ArrayList 或数组,且对性能有极致要求 |
传统 for |
理论上性能略优,但通常可以忽略,优先考虑代码清晰度。 |
Java 9+ 的改进
在 Java 9 中,引入了新的特性,使得 for-each 可以用于某些非 Iterable 的对象,Stream,这进一步模糊了界限,但核心思想不变:用最合适的工具做最合适的事。
// Java 9+ 可以直接遍历 Stream
Stream<String> stream = Stream.of("A", "B", "C");
for (String s : stream.toList()) { // 先转换为 List
System.out.println(s);
}
for-each 循环是 Java 5 引入的一个语法糖,极大地简化了遍历集合和数组的操作,是日常开发中的首选,它更安全、更易读。
传统 for 循环并未过时,它在需要精细控制遍历过程(如索引、顺序)和修改集合内容的场景下,仍然是不可或缺的工具。
优秀的 Java 开发者应该能够根据具体需求,灵活地选择最合适的循环方式。没有绝对的好坏,只有是否合适。
