凡是实现 Collection
接口的集合类都有一个 iterator
方法,会返回一个实现了 Iterator
接口的对象,用于遍历集合。Iterator
接口主要有三个方法,分别是 hasNext
、next
、remove
方法。java
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
:spa
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
)的集合类,在遍历集合时不是直接访问原有集合,而是先将原有集合的内容复制一份,而后在拷贝的集合上进行遍历。因为是对拷贝的集合进行遍历,因此在遍历过程当中对原集合的修改并不会被迭代器检测到,因此不会抛出 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"
。缘由是开始时 List
的 size
为 4
,从前日后,循环到了索引 #1
,发现符合条件,因而删除了 #1
的元素。此时 List
的 size
变为 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
是 JDK1.0
引入的接口,为集合提供遍历的接口,使用它的集合包括 Vector
、HashTable
等。Enumeration
迭代器不支持 fail-fast
机制。
它只有两个接口方法:hasMoreElements
、nextElement
用来判断是否有元素和获取元素,但不能对数据进行修改。
但须要注意的是 Enumeration
迭代器只能遍历 Vector
、HashTable
这种古老的集合,所以一般状况下不要使用。
方法一 在 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
,则使用方法三;