详解迭代器Iterator
一、迭代器Iterator
集合接口Collection继承了接口Iterable,该接口提供了一个用于获取迭代器Iterator的方法,使用迭代器我们可以逐个访问集合中的元素。下面是迭代器接口源码:
1 public interface Iterator<E> { 2 3 boolean hasNext(); 4 5 E next(); 6 7 void remove(); 8 }
需要注意的是,Map接口并未继承Iterable,因此Map无法获取到迭代器。
1.1.向后遍历元素
首先来看看两个相关API的功能:
- hasNext():询问是否还有下一个元素,返回true表示有,反之表示没有。
- next():移动迭代器游标,跨过下一个元素并返回该元素的引用,如果不存在下一个元素,方法会抛出"NoSuchElementException"。
所以,我们一般通过组合使用hasNext()与next()方法,逐个访问集合中的元素,元素的访问顺序依赖于集合的具体实现,比如List是按照放入顺序访问,Set访问顺序则不一定(可能是有序的TreeSet,可能是无序的HashSet),下面是一段示例代码:
while (iter.hasNext()) { String element = it.next(); //do something with element }
从JavaSE5.0起,上面这个循环可以采用一种更优雅的缩写形式,即foreach循环。编译器在编译时会简单的将foreach循环翻译成迭代器循环,因此,foreach循环支持任何实现了Iterable接口的对象。
1.2.删除元素
使用remove删除集合中的元素:
- remove():删除上次调用next()方法时返回的元素。需要注意,一次next()只支持一次remove(),如果次数无法匹配,remove操作将会抛出"IllegalStateException",比如:
iter.next(); iter.remove(); iter.remove(); //抛出IllegalStateException
二、List迭代器
Collection接口的子接口List提供了一个方法listIterator(),用于返回Iterator的子接口ListIterator对象。
1 ListIterator<E> listIterator();
相比于Iterator,ListIterator添加了从后向前遍历、获取迭代索引以及添加、设置元素的功能。
1 public interface ListIterator<E> extends Iterator<E> { 2 3 boolean hasPrevious(); 4 5 E previous(); 6 7 int nextIndex(); 8 9 int previousIndex(); 10 11 void add(E e); 12 13 void set(E e); 14 15 }
2.1.向前遍历元素
使用方式类比于向后遍历,previous()方法用于向前跨越一个元素并返回该元素的引用,hasPrevious()用于询问前面是否还有元素。
2.2.获取迭代索引
获取迭代索引的功能只与当前迭代器游标所处位置有关,与是否向前遍历还是向后遍历无关:
- nextIndex():返回下一次调用next()方法时返回的元素的索引。
- previousIndex():返回下一次调用previous()方法时返回的元素的索引。
2.3.添加元素
add方法在迭代器当前位置之前添加一个新对象,例如下面的代码将越过列表中的第一个元素,并在第二个元素之前添加Juliet。
1 List<String> staff = new LinkedList<>(); 2 staff.add("Amy"); 3 staff.add("Bob"); 4 staff.add("Carl"); 5 ListIterator<String> iter = staff.listIterator(); 6 iter.next(); 7 iter.add("Juliet");
可以看到,Juliet成为第二个元素,而Bob和Carl的位置依次向后移动一位。
注意,与remove方法不同的是,add方法不与next()进行绑定。也就是说,使用add没有任何需要移动迭代器位置的前提条件,并且可以一直添加元素,在上面的示例中一次可以添加Juliet、Green、Lucy等人,列表变成如下样子:
2.4.设置元素
Set方法使用一个新元素取代调用next()或previous()方法返回的上一个元素,例如:
1 ListIterator<String> iter = list.listIterator(); 2 String oldValue = iter.next(); 3 iter.set(newValue);
newValue将会替换掉oldValue这个元素。
三、关于ConcurrentModificationException
有时候,在使用foreach或iterator遍历时会抛出ConcurrentModificationException,该异常产生的原因是:迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的方法修改了。下面我们详细解释一下这种发现机制是如何实现的?
集合对改写操作维护一个计数(改写操作指的是对集合结构性的修改,如添加、删除元素,而修改元素内容、替换元素不被视为结构性修改),从该集合获得的每一个迭代器与集合自身API的改写操作都会累加这个计数。同时,每个迭代器自身也独自维护一个计数值,每一次改写操作也会累加自身的计数值。迭代器的每个方法开始处都会检查自己的改写计数值与集合的改写计数值是否一致,如果不一致,就抛出异常ConcurrentModificationException。
根据上述的原理,我们解释一下如下代码产生ConcurrentModificationException的原因:我们知道,foreach循环本质上是一个迭代器遍历。第一次遍历获取到element时,迭代器自身计数和集合计数均为0,在循环体中调用了集合自身方法删除元素后,集合计数被修改为1。在下一次执行迭代器方法hasNext()时(编译器编译结果),迭代器发现自身计数与集合计数不一致,抛出异常。不过,需要值得注意的细节是,第一次remove是执行成功的,首元素已被删掉,在实际开发中一定要注意这个问题。
1 List<String> list = new ArrayList<>(); 2 list.add("a"); 3 list.add("a"); 4 list.add("a"); 5 for (String element : list) { 6 list.remove(element); 7 }