Java迭代器看这篇就够了!

204 阅读6分钟

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以记录当前的枚举进度。这个接口有两个缺点:

  1. 通过下标访问使得枚举器不支持删除元素。
  2. 方法名称太长了。枚举器的设计目的是简化遍历,太长的方法名使得其使用臃肿。

于是,迭代器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接口指定,基本每一个集合都会有。