Iterator 与 ListIterator
凡是实现 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)
快速失败机制(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)
采用安全失败机制(fail—safe
)的集合类,在遍历集合时不是直接访问原有集合,而是先将原有集合的内容复制一份,然后在拷贝的集合上进行遍历。
由于是对拷贝的集合进行遍历,所以在遍历过程中对原集合的修改并不会被迭代器检测到,所以不会抛出 ConcurrentModificationException
异常。
虽然基于拷贝内容的安全失败机制避免了 ConcurrentModificationException
,但是迭代器并不能访问到修改后的内容,而仍然是开始遍历那一刻拿到的集合拷贝。
在 java.util.concurrent
包下的集合都采用安全失败机制,所以可以在多线程场景下进行并发使用和修改操作。
如何在遍历集合的同时删除元素
在遍历集合时,正确的删除方式有以下几种:
普通 for 循环从后往前遍历
使用普通 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 删除后跳出循环
在使用 foreach
迭代器遍历集合时,在删除元素后使用 break 跳出循环,则不会触发 fail-fast
。
for (String str : stringList) {
if ("abc".equals(str)) {
stringList.remove(str);
break;
}
}
复制代码
使用迭代器自带的 remove 方法
Iterator<String> iterator = stringList.iterator();
while (iterator.hasNext()) {
String str = iterator.next();
if ("abc".equals(str)) {
iterator.remove(); // 这里是 iterator,而不是 stringList
break;
}
}
复制代码
Enumeration
Enumeration
是 JDK1.0
引入的接口,为集合提供遍历的接口,使用它的集合包括 Vector
、HashTable
等。Enumeration
迭代器不支持 fail-fast
机制。
它只有两个接口方法:hasMoreElements
、nextElement
用来判断是否有元素和获取元素,但不能对数据进行修改。
但需要注意的是 Enumeration
迭代器只能遍历 Vector
、HashTable
这种古老的集合,因此通常情况下不要使用。
Java中遍历 Map 的几种方式
方法一 在 for-each 循环中使用 entries 来遍历
这是最常见的,并且在大多数情况下也是最可取的遍历方式,在键和值都需要时使用。
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
,因此在遍历前应该检查是否为空引用。
方法二 在 for-each 循环中遍历 keys 或 values
如果只需要 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
遍历在性能上稍好,而且代码更加干净。
方法三 使用 Iterator 遍历
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
,则使用方法三; - 如果键值都需要,则使用方法一。