Java迭代器看这篇就够了!
在讲Java迭代器interface Iterator<E>之前,先讲一讲Java枚举接口interface Enumeration<E>。
枚举器Enumeration
这和Java中的枚举类是两个概念,这个接口用来枚举(也就是遍历)Java集合。实现枚举接口的对象每次生成一系列元素。对nextElement方法的连续调用会返回系列中的连续元素。
例如,要打印Vector v 的所有元素,可以使用以下方法:
for (Enumeration<E> e = v.elements(); e.hasMoreElements(); ) {
System.out.println(e.nextElement());
}
该接口的方法十分简单,只有hasMoreElements和nextElement两个方法。
除此之外还有一个Iterator asIterator方法,用于将Enumeration转换为Iterator。
它的实现十分简单,Vector中的枚举器实现的源码是这样的:
public Enumeration<E> elements() {
return new Enumeration<E>() {
int count = 0;
public boolean hasMoreElements() {
return count < elementCount;
}
public E nextElement() {
synchronized (Vector.this) {
if (count < elementCount) {
return elementData(count++);
}
}
throw new NoSuchElementException("Vector Enumeration");
}
};
}
E elementData(int index) {
return (E) elementData[index];
}
该方法返回一个枚举器,枚举器中维护了下标变量count以记录当前的枚举进度。这个接口有两个缺点:
- 通过下标访问使得枚举器不支持删除元素。
- 方法名称太长了。枚举器的设计目的是简化遍历,太长的方法名使得其使用臃肿。
于是,迭代器iterator应运而生。
迭代器Iterator
迭代器中当然也有检查是否含有更多元素和获取下一个元素的方法。方法名被简化为hasNext和next。除此之外,新增方法remove(默认不支持,要求重写),和forEachRemaining,对每个剩余元素执行给定的操作,直到处理完所有元素或该操作抛出异常。
依然是Vector,让我们来看看它的迭代器实现。
hasNext和next的实现类似于枚举器。而remove方法的实现关键在于先调用Vector.this.remove(lastRet)移除当前指针所指的元素,然后修改当前指针为-1,代表元素不存在,修改下一个元素指针为所移除的元素的下标。对于forEachRemaining方法,要求传入参数Consumer<? super E> action,然后依次调用action.accept(elementAt(es, i++));,对剩余每个元素做相应处理。
消费者接口Consumer
该接口包含void accept(T t)和默认方法Consumer andThen(Consumer<? super T> after)。默认方法andThen的行为是:返回一个组成的Consumer,该Consumer依次执行此操作和之后的操作,故支持链式调用。很明显这是一个函数式接口,支持lambda语句。以下是一个Consumer接口的实现实例。
public class Person {
int age;
Person(int age){
this.age = age;
}
@Override
public String toString(){
return "age:"+age;
}
public static void main(String[] args) {
Vector<Person> v = new Vector<>();
v.add(new Person(10));
v.add(new Person(20));
//年龄+1
v.iterator().forEachRemaining(
p -> p.age++
);
v.forEach(System.out::println);
}
}
除此之外,我们还可能看到另一种奇怪的用法:v.forEach(System.out::println);forEach不是要求传入一个需要重写的Consumer接口吗?
这是 Java 中的一种简洁写法,它利用了方法引用(Method Reference)的特性。篇幅有限,这里只简单说一下:lambda表达式的核心在于将方法当作参数传递,如果有现成的方法,那么直接引用过来就可以了。我会在另一篇文章中详细介绍方法引用和它的使用场景。
迭代器并发问题
前面看到,无论是枚举器还是迭代器,内部都维护了集合的下标。也就是说它们是有状态的对象,那么它们是否线程安全呢?
迭代器本身不支持多线程调用。它们会在每个方法执行之前检查集合的modCount字段是否发生了变化,以防止遍历过程中存在以下行为:
- 在该迭代器以外的任何地方发生对集合的结构性修改。
解释:
- modCount是该列表被结构性修改的次数。
- 结构修改是指改变列表的大小,或以其他方式对其进行扰动,从而使正在进行的迭代可能产生错误的结果。
| 操作 | 结构性修改 | 说明 |
|---|---|---|
| 添加元素 | 是 | 改变了列表的大小。 |
| 删除元素 | 是 | 改变了列表的大小。 |
| 清空列表 | 是 | 移除了所有元素。 |
| 主动扩容 | 否 | 主动扩容不会更新 modCount。 |
| 更改元素值 | 否 | 元素数量和存储结构未改变,仅修改内容。 |
| 替换整个元素引用 | 否 | 仅修改了某个位置的引用,元素数量未变。 |
| 获取元素值 | 否 | 不涉及修改操作,仅为读取。 |
| 遍历列表 | 否 | 不改变列表内容或结构,仅访问元素。 |
也就是说,即使是单线程环境下,在创建一个迭代器后,未使用该迭代器改变了列表结构,这个迭代器将会失效。更别说多线程环境了。
以下是官方文档的解释:
iterator和listIterator方法返回的迭代器和列表迭代器实现将使用modCount字段。如果该字段的值发生意外变化,迭代器(或列表迭代器)将在执行下一个、移除、上一个、设置或添加操作时抛出并发修改异常(ConcurrentModificationException)。这提供了快速故障行为,而不是在迭代过程中面对并发修改时的非确定行为。 子类对该字段的使用是可选的。如果子类希望提供故障快速迭代器(和列表迭代器),那么它只需在add(int, E)和remove(int)方法(以及任何其他会导致列表结构修改的重载方法)中递增该字段即可。对add(int, E)或remove(int)的单次调用对该字段的增量不得超过 1,否则迭代器(和列表迭代器)将抛出虚假的ConcurrentModificationException 异常。如果实现不希望提供故障快速迭代器,则可以忽略此字段。
JDK中当然有“不希望提供故障快速迭代器”的集合类。CopyOnWriteArrayList就是一个例子。由于COW写时复制的特性,它的迭代器在被创建时内部持有了一个原列表的副本,且不支持修改,只能读。这样的基本无状态的迭代器自然是支持多线程的。
增强for循环语法糖
for (Person person : v) {
System.out.println(person);
}
//对于这样的语法糖,它在编译后会变为:
for (Iterator<Person> i = v.iterator();i.hasNext();) {
Person person = i.next();
System.out.println(person);
}
增强for循环的底层原理就是这样,很简单。从中我们可以看出,增强for循环的遍历顺序是完全依照于该集合的迭代器的,且这个迭代器是由Iterable接口的iterator方法指定的。
ListIterator
ListIterator是对Iterator的扩展,补全了其缺失的一些方法:
- 向前遍历的hasPrevious,previous方法。
- 替换和增加用的set,add方法。
ListIterator可以通过集合的listIterator方法得到,该方法由List接口指定,基本每一个集合都会有。