Java 集合(2)之 Iterator 迭代器

Iterator 与 ListIterator

凡是实现 Collection 接口的集合类都有一个 iterator 方法,会返回一个实现了 Iterator 接口的对象,用于遍历集合。Iterator 接口主要有三个方法,分别是 hasNextnextremove 方法。

ListIterator 继承自 Iterator,专门用于实现 List 接口对象,除了 Iterator 接口的方法外,还有其他几个方法。

基于顺序存储集合的 Iterator 可以直接按位置访问数据。基于链式存储集合的 Iterator,一般都是需要保存当前遍历的位置,然后根据当前位置来向前或者向后移动指针。

IteratorListIterator 的区别:

  • Iterator 可用于遍历 SetListListIterator 只可用于遍历 List
  • Iterator 只能向后遍历;ListIterator 可向前或向后遍历。
  • ListIterator 实现了 Iterator 的接口,并增加了 addsethasPreviouspreviouspreviousIndexnextIndex 方法。

快速失败(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

EnumerationJDK1.0 引入的接口,为集合提供遍历的接口,使用它的集合包括 VectorHashTable 等。Enumeration 迭代器不支持 fail-fast 机制。

它只有两个接口方法:hasMoreElementsnextElement 用来判断是否有元素和获取元素,但不能对数据进行修改。

但需要注意的是 Enumeration 迭代器只能遍历 VectorHashTable 这种古老的集合,因此通常情况下不要使用。

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 中的键或者值,可以通过 keySetvalues 来实现遍历,而不是用 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,则使用方法三;
  • 如果键值都需要,则使用方法一。

猜你喜欢

转载自juejin.im/post/5c6d1f08e51d457fbf5de648