Java 集合(2)之 Iterator 迭代器

Iterator 与 ListIterator

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

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

基于顺序存储集合的 Iterator 能够直接按位置访问数据。基于链式存储集合的 Iterator,通常都是须要保存当前遍历的位置,而后根据当前位置来向前或者向后移动指针。多线程

IteratorListIterator 的区别:并发

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

快速失败(fail—fast)

快速失败机制(fail—fast)就是在使用迭代器遍历一个集合对象时,若是遍历过程当中对集合进行修改(增删改),则会抛出 ConcurrentModificationException 异常。性能

例如如下代码,就会抛出 ConcurrentModificationExceptionspa

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 包下的集合类都采用快速失败机制,因此在多线程下,不能发生并发修改,也就是在迭代过程当中不能被修改。code

安全失败(fail—safe)

采用安全失败机制(fail—safe)的集合类,在遍历集合时不是直接访问原有集合,而是先将原有集合的内容复制一份,而后在拷贝的集合上进行遍历。因为是对拷贝的集合进行遍历,因此在遍历过程当中对原集合的修改并不会被迭代器检测到,因此不会抛出 ConcurrentModificationException 异常。对象

虽然基于拷贝内容的安全失败机制避免了 ConcurrentModificationException,可是迭代器并不能访问到修改后的内容,而仍然是开始遍历那一刻拿到的集合拷贝。

java.util.concurrent 包下的集合都采用安全失败机制,因此能够在多线程场景下进行并发使用和修改操做。

如何在遍历集合的同时删除元素

在遍历集合时,正确的删除方式有如下几种:

普通 for 循环

在使用普通 for 循环时,若是从前日后遍历:

ArrayList<String> stringList = new ArrayList<>();
stringList.add("abc");
stringList.add("def");
stringList.add("def");
stringList.add("ghi");

for (int i = 0;i < stringList.size(); i++) {
    String str = stringList.get(i);
    if ("def".equals(str)) {
        stringList.remove(str);
    }
}
复制代码

打印结果为:

abc def ghi
复制代码

能够看到,这里跳过了第二个 "def"。缘由是开始时 Listsize4,从前日后,循环到了索引 #1,发现符合条件,因而删除了 #1 的元素。此时 Listsize 变为 3,索引 #1 就指向了以前 #2 的元素(就是 #2 的元素移动了 #1#3 移动到了 #2)。

而下一次循环会从索引 #2 开始,查看的是删除以前 #3 的元素,因而以前 #2 的元素(左移到了 #1)就被跳过了。

而若是从后往前遍历,就能够避免元素移动形成的影响。

ArrayList<String> stringList = new ArrayList<>();
stringList.add("abc");
stringList.add("def");
stringList.add("def");
stringList.add("ghi");

for (int i = stringList.size() - 1;i >= 0; i--) {
    String str = stringList.get(i);
    if ("abc".equals(str)) {
        stringList.remove(str);
    }
}
// abc ghi
复制代码

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
    }
}  
复制代码

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,则使用方法三;
  • 若是键值都须要,则使用方法一。
相关文章
相关标签/搜索