集合之迭代器

1,032 阅读10分钟

前言

    集合作为Java基础内容中的重点内容,在我们的日常开发中不可避免的使用,因而最近研究一下Java集合的源码,在此分享给大家。

集合的继承实现关系

打开IDEA查看集合的继承实现关系,关系图如下: image.png

图一

    我们可以看到集合的根接口Collection继承Iterable接口,这也是集合能够通过迭代器Iterator遍历的关键所在。本篇就为大家带来集合中使用的设计模式———迭代器模式。

迭代器模式——对象行为型模式

23种设计模式最早是在《设计模式:可复用面向对象软件的基础》一书中提出的。 关于迭代器模式,该书中描述如下:

提供一种方法顺序访问一个聚合对象中的各个对象,而又不需暴露该对象的内部表示。

    简而言之,迭代器模式是一种统一的遍历模式,其主要思想是将对列表的遍历和访问从列表对象中分离出来,放入迭代器中,迭代器封装遍历的接口。

    换言之将遍历集合的责任封装到迭代器这个对象中,对象内部实现遍历,而使用者无需关心遍历对象的内部结构。

    正如我们在使用ArrayList、LinkedList时,我们都可以直接拿到其所属的迭代器Iterator去进行遍历,而不需要关心内部的具体结构。

Iterable和Iterator

在学习集合中的迭代器之前,我们先学习一下Iterable和Iterator。

Iterator接口

Iterator定义如下:

package java.util;
import java.util.function.Consumer;

public interface Iterator<E> {

    boolean hasNext();

    E next();

    default void remove() {
        throw new UnsupportedOperationException("remove");
    }

    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

一共有四个方法,其含义如下:

  • hasNext():判断迭代器是否还存在剩余未遍历元素;
  • next():返回当前迭代器下一个未遍历元素;
  • remove():删除当前迭代器返回的元素,注意该方法需要在next()执行后,任何操作之前。
  • forEachRemaining():JDK8新增方法,这个方法提供了一种更简洁、更灵活的方式来遍历集合中剩余的元素,特别是与 Lambda 表达式或方法引用结合使用时,可以极大地提高代码的可读性和简洁性。

Iterable接口

Iterable接口,查看JDK源码描述如下:

Implementing this interface allows an object to be the target of the "for-each loop" statement. See For-each Loop

即实现了Iterable接口可以成为for-each loop的目标。即可以使用for-each进行遍历。 该接口源码如下:

package java.lang;

import java.util.Iterator;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
public interface Iterable<T> {
    Iterator<T> iterator();

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
    
    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }
}

    其中两个方法已经有默认实现,只有 iterator() 方法要需要实现类必须去是实现的。

    实现类可以通过实现iterator方法,来获取自己类的迭代器,以自己的迭代逻辑进行迭代,如果需要增加迭代方式,只需要增加迭代器就好。

    如:我们可以写一个Tree,实现Iterable接口,可以在iterator里的next()方法进行自己的逻辑遍历,如前序遍历,同样可以增加新的iterator实现后序遍历。

    这种迭代器模式十分利于扩展。像Java的集合,如ArrayList、LinkedList等均是如此实现的。接下来我们就谈一谈集合中的迭代器。

集合中的迭代器模式

    如图一所示继承关系,我们能狗看到根接口Collection实现了Iterable接口。也因此所有的Collection的子类都可以重写iterator方法,实现各自的迭代逻辑。

ArrayList

类图结构

image.png 可以看到ArrayList继承根接口Collection,接下来我们就一起探究一下ArrayList的迭代器实现。

源码解析

iterator源码

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

public Iterator<E> iterator() {
    return new Itr();
}

/**
 * An optimized version of AbstractList.Itr
 */
private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    Itr() {}

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public void forEachRemaining(Consumer<? super E> consumer) {
        Objects.requireNonNull(consumer);
        final int size = ArrayList.this.size;
        int i = cursor;
        if (i >= size) {
            return;
        }
        final Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length) {
            throw new ConcurrentModificationException();
        }
        while (i != size && modCount == expectedModCount) {
            consumer.accept((E) elementData[i++]);
        }
        // update once at end of iteration to reduce heap write traffic
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

在ArrayList的内部,实现了一个自己的迭代器继承自Iterator,并重写了相关方法。

内部迭代器属性
cursor:指向下一个元素的索引 ;
lastRet:指向当前返回元素的索引;
expectedModCount:与modCount比较,判断迭代器移除元素前,可变数组是否被修改。

方法
hasNext():将指向下一个元素的索引与了可变数组的尺寸作比较;
next():返回下一个元素;
remove():移除当前返回的元素,在移除前会判断可变数组是否被修改,如果被修改则抛出异常;否则会调用可变数组的remove方法进行移;。 forEachRemaining():这个方法展示了如何利用Java的Consumer接口来遍历集合中剩余的元素,并将其作为参数传递给给定的消费者函数执行自定义操作。

  • 首先,通过Objects.requireNonNull(consumer)检查传入的consumer是否为null,以避免空指针异常。
  • 定义局部变量size来存储当前ArrayList的大小,以及i作为迭代的索引,初始化为迭代器的当前位置(cursor)。
  • 检查迭代器的当前索引i是否已经超过了集合的大小size。如果超过,则说明已无剩余元素需要遍历,直接返回。
  • 获取ArrayList底层的元素数组elementData,并检查索引i是否在其有效范围内。如果不在此范围内,说明集合在迭代过程中被非法修改,抛出ConcurrentModificationException异常。
  • 进入一个循环,条件是在索引i未达到集合大小size且集合的修改次数modCount与迭代器期望的修改次数expectedModCount相等时继续。这两个条件保证了迭代过程中的线程安全,检测到并发修改时会抛出异常。
  • 在循环体内,使用consumer.accept((E) elementData[i++])调用消费者函数处理当前元素,并将索引i递增,以便下一次迭代处理下一个元素。
  • 循环结束后,更新迭代器的状态:将cursor设置为最终的索引位置ilastRet设置为处理的最后一个元素的索引(即i - 1),然后调用checkForComodification()方法来最终检查是否有并发修改发生。这个方法是内部用来确保迭代过程中集合没有被其他线程修改的额外保障。

整个方法实现了高效且安全地遍历ArrayList中剩余的所有元素,并将它们传递给指定的消费者函数进行处理。

ArrayList除了实现Iterable的迭代器还有继承自AbstractList的两个迭代器

listIterator源码

public ListIterator<E> listIterator(int index) {
    if (index < 0 || index > size)
        throw new IndexOutOfBoundsException("Index: "+index);
    return new ListItr(index);
}

public ListIterator<E> listIterator() {
    return new ListItr(0);
}

private class ListItr extends Itr implements ListIterator<E> {
    ListItr(int index) {
        super();
        cursor = index;
    }

    public boolean hasPrevious() {
        return cursor != 0;
    }

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor - 1;
    }

    @SuppressWarnings("unchecked")
    public E previous() {
        checkForComodification();
        int i = cursor - 1;
        if (i < 0)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i;
        return (E) elementData[lastRet = i];
    }

    public void set(E e) {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.set(lastRet, e);
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    public void add(E e) {
        checkForComodification();

        try {
            int i = cursor;
            ArrayList.this.add(i, e);
            cursor = i + 1;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
}

ListItr内部类,它是针对ArrayList实现的双向迭代器(ListIterator)。这个迭代器不仅支持向前遍历,还支持向后遍历,以及在迭代过程中修改元素和插入元素等操作。下面是这个类各个方法的简要说明:

  1. 构造方法 ListItr(int index) :通过给定的索引index初始化迭代器,使得迭代可以从列表的指定位置开始。
  2. hasPrevious() :判断是否还有前一个元素。如果当前索引cursor不为0,表示可以向前遍历。
  3. nextIndex() :返回调用next()方法会返回的元素的索引位置,也就是当前游标的位置。
  4. previousIndex() :返回调用previous()方法会返回的元素的索引位置,即当前游标减1。
  5. previous() :返回列表中的前一个元素。首先检查并发修改,然后调整游标并返回前一个元素。若不存在前一个元素,则抛出NoSuchElementException异常;若检测到并发修改,则抛出ConcurrentModificationException异常。
  6. set(E e) :替换迭代器上一次返回的元素。需确保已调用过next()previous()方法返回过元素(即lastRet >= 0),并检查并发修改。尝试替换元素,若索引越界则抛出ConcurrentModificationException
  7. add(E e) :在当前游标位置插入一个元素,并将游标移动到新插入元素的下一个位置,同时更新lastRet标记为无效(-1),并同步预期修改计数以反映列表的实际修改状态。同样,此操作会检查并发修改异常。

ListItr类为ArrayList提供了丰富的双向迭代能力及修改功能。

LinkedList

类图结构

image.png

源码解析

通过上述的类图结构,可以看到LinkedList继承自AbstractSwquentialList。 而iterator方法也正是在AbstractSwquentialList中实现的。

public Iterator<E> iterator() {
    return listIterator();
}

public abstract ListIterator<E> listIterator(int index);

可以看到iterator方法调用的是一个抽象方法,此抽象方法实现于LinkedList。源码如下:

public ListIterator<E> listIterator(int index) {
    checkPositionIndex(index);
    return new ListItr(index);
}

private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;
    private Node<E> next;
    private int nextIndex;
    private int expectedModCount = modCount;

    ListItr(int index) {
        // assert isPositionIndex(index);
        next = (index == size) ? null : node(index);
        nextIndex = index;
    }

    public boolean hasNext() {
        return nextIndex < size;
    }

    public E next() {
        checkForComodification();
        if (!hasNext())
            throw new NoSuchElementException();

        lastReturned = next;
        next = next.next;
        nextIndex++;
        return lastReturned.item;
    }

    public boolean hasPrevious() {
        return nextIndex > 0;
    }

    public E previous() {
        checkForComodification();
        if (!hasPrevious())
            throw new NoSuchElementException();

        lastReturned = next = (next == null) ? last : next.prev;
        nextIndex--;
        return lastReturned.item;
    }

    public int nextIndex() {
        return nextIndex;
    }

    public int previousIndex() {
        return nextIndex - 1;
    }

    public void remove() {
        checkForComodification();
        if (lastReturned == null)
            throw new IllegalStateException();

        Node<E> lastNext = lastReturned.next;
        unlink(lastReturned);
        if (next == lastReturned)
            next = lastNext;
        else
            nextIndex--;
        lastReturned = null;
        expectedModCount++;
    }

    public void set(E e) {
        if (lastReturned == null)
            throw new IllegalStateException();
        checkForComodification();
        lastReturned.item = e;
    }

    public void add(E e) {
        checkForComodification();
        lastReturned = null;
        if (next == null)
            linkLast(e);
        else
            linkBefore(e, next);
        nextIndex++;
        expectedModCount++;
    }

    public void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (modCount == expectedModCount && nextIndex < size) {
            action.accept(next.item);
            lastReturned = next;
            next = next.next;
            nextIndex++;
        }
        checkForComodification();
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

上述代码实现了一个双向迭代器。这里的迭代器是为一个自定义的链表结构设计的,其中每个节点(Node<E>)包含item(元素本身),next(指向下一个节点的引用),以及prev(指向前一个节点的引用)。下面是关键部分的解释:

  • 构造方法 ListItr(int index) :根据给定的索引index初始化迭代器,找到对应的起始节点next,并设置nextIndex
  • hasNext()hasPrevious() :分别检查是否还有下一个元素或前一个元素可供迭代。
  • next()previous() :返回下一个或前一个元素,并更新迭代器的状态(如移动到下一个节点,更新索引等)。同时,会检查并发修改异常。
  • nextIndex()previousIndex() :返回调用next()previous()后将返回的元素的索引位置。
  • remove() :移除最近由next()previous()返回的元素,并更新迭代器状态。
  • set(E e) :替换最近由next()previous()返回的元素。
  • add(E e) :在当前迭代位置插入一个新元素,并更新迭代器状态。
  • forEachRemaining(Consumer<? super E> action) :使用Lambda表达式或方法引用来遍历剩余的元素。
  • checkForComodification() :这是一个关键方法,用于检查集合的修改次数(modCount)是否与迭代器期望的修改次数(expectedModCount)相匹配,以确保迭代过程中的线程安全,若不匹配则抛出ConcurrentModificationException异常。

这个实现展示了如何在链表数据结构上构建一个功能全面的双向迭代器,支持正向和反向遍历、元素修改和新增,同时确保了迭代过程中的并发修改控制。