凡是实现 Collection
接口的集合类都有一个 iterator
方法,会返回一个实现了 Iterator
接口的对象,用于遍历集合。 Iterator
接口主要有三个方法,分别是 hasNext
、 next
、 remove
方法。
ListIterator
继承自 Iterator
,专门用于实现 List
接口对象,除了 Iterator
接口的方法外,还有其他几个方法。
基于顺序存储集合的 Iterator
可以直接按位置访问数据。基于链式存储集合的 Iterator
,一般都是需要保存当前遍历的位置,然后根据当前位置来向前或者向后移动指针。
Iterator
与 ListIterator
的区别:
Iterator
可用于遍历 Set
、 List
; ListIterator
只可用于遍历 List
。 Iterator
只能向后遍历; ListIterator
可向前或向后遍历。 ListIterator
实现了 Iterator
的接口,并增加了 add
、 set
、 hasPrevious
、 previous
、 previousIndex
、 nextIndex
方法。
快速失败机制( fail—fast
)就是在使用迭代器遍历一个集合对象时,如果遍历过程中对集合进行修改(增删改),则会抛出 ConcurrentModificationException
异常。
例如以下代码,就会抛出 ConcurrentModificationException
:
List<String> stringList = new ArrayList<>(); stringList.add("abc"); stringList.add("def"); Iterator<String> iterator = stringList.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); stringList.add("ghi"); } 复制代码
查看 ArrayList
源码,就可以知道为什么会抛出异常。原因是在 ArrayList
类的内部类迭代器 Itr
中有一个 expectedModCount
变量。在 AbstracList
抽象类有一个 modCount
变量,集合在被遍历期间如果内容发生变化,就会改变 modCount
的值。每当迭代器使用 next()
遍历下一个元素之前,都会检测 modCount
变量是否等于 expectedmodCount
,如果相等就继续遍历;否则就会抛出异常。
final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); } 复制代码
注意:这里异常的抛出条件是检测到 modCount != expectedmodCount
。如果集合发生变化时将 modCount
的值又刚好设置为 expectedmodCount
,那么就不会抛出异常。因此,不能依赖于这个异常是否抛出而进行并发操作,这个异常只建议使用于检测并发修改的 bug
。
在 java.util
包下的集合类都采用快速失败机制,所以在多线程下,不能发生并发修改,也就是在迭代过程中不能被修改。
采用安全失败机制( fail—safe
)的集合类,在遍历集合时不是直接访问原有集合,而是先将原有集合的内容复制一份,然后在拷贝的集合上进行遍历。
由于是对拷贝的集合进行遍历,所以在遍历过程中对原集合的修改并不会被迭代器检测到,所以不会抛出 ConcurrentModificationException
异常。
虽然基于拷贝内容的安全失败机制避免了 ConcurrentModificationException
,但是迭代器并不能访问到修改后的内容,而仍然是开始遍历那一刻拿到的集合拷贝。
在 java.util.concurrent
包下的集合都采用安全失败机制,所以可以在多线程场景下进行并发使用和修改操作。
在遍历集合时,正确的删除方式有以下几种:
使用普通 for
循环,如果从后往前遍历,则可以避免元素移动的影响。
ArrayList<String> stringList = new ArrayList<>(); stringList.add("abc"); stringList.add("def"); for (int i = 0;i < stringList.size(); i++) { String str = stringList.get(i); if ("abc".equals(str)) { stringList.remove(str); break; } } 复制代码
在使用 foreach
迭代器遍历集合时,在删除元素后使用 break 跳出循环,则不会触发 fail-fast
。
for (String str : stringList) { if ("abc".equals(str)) { stringList.remove(str); break; } } 复制代码
Iterator<String> iterator = stringList.iterator(); while (iterator.hasNext()) { String str = iterator.next(); if ("abc".equals(str)) { iterator.remove(); // 这里是 iterator,而不是 stringList break; } } 复制代码
Enumeration
是 JDK1.0
引入的接口,为集合提供遍历的接口,使用它的集合包括 Vector
、 HashTable
等。 Enumeration
迭代器不支持 fail-fast
机制。
它只有两个接口方法: hasMoreElements
、 nextElement
用来判断是否有元素和获取元素,但不能对数据进行修改。
但需要注意的是 Enumeration
迭代器只能遍历 Vector
、 HashTable
这种古老的集合,因此通常情况下不要使用。
这是最常见的,并且在大多数情况下也是最可取的遍历方式,在键和值都需要时使用。
Map<Integer, Integer> map = new HashMap<>(); for (Map.Entry<Integer, Integer> entry : map.entrySet()) { System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue()); } 复制代码
注意:如果遍历一个空 map
对象, for-each
循环将抛出 NullPointerException
,因此在遍历前应该检查是否为空引用。
如果只需要 map
中的键或者值,可以通过 keySet
或 values
来实现遍历,而不是用 entrySet
。
Map<Integer, Integer> map = new HashMap<Integer, Integer>(); //遍历 map 中的键 for (Integer key : map.keySet()) { System.out.println("Key = " + key); } //遍历 map 中的值 for (Integer value : map.values()) { System.out.println("Value = " + value); } 复制代码
该方法比 entrySet
遍历在性能上稍好,而且代码更加干净。
Map<Integer, Integer> map = new HashMap<Integer, Integer>(); Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator(); while (entries.hasNext()) { Map.Entry<Integer, Integer> entry = entries.next(); System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue()); } 复制代码
这种方式看起来冗余却有其优点所在,可以在遍历时调用 iterator.remove()
来删除 entries
,另两个方法则不能。
从性能方面看,该方法类同于 for-each
遍历(即方法二)的性能。
keys
)或值( values
),则使用方法二; entries
,则使用方法三;