ArrayList源码解析(二):迭代器,subList

236 阅读8分钟

前言

  接着我的上一篇博客 ArrayList源码学习(一):初始化,扩容以及增删改查,这篇博客还是从源码介绍下ArrayList的迭代器和subList,也是给自己做个总结,方便自己以后复习,如果有我理解的不对的地方,可以评论区友好交流。

迭代器

Iterator

初始化和内部属性

  想得到一个Iterator,只需要调用iterator函数即可,它是public的,如下:

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

  可以看出,返回了一个Itr类的对象,这个Itr,即是ArrayList类的一个内部类:

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() {}

    }
}

  从Itr的属性可以发现,一个迭代器对象本身并不存储什么实际的数据,只是通过内部的cursor,lastRet,expectedModCount 提供一种迭代ArrayList对象的机制。

  这个Itr类实现了Iterator接口,本身提供的方法也比较少,只有4个。

next方法

public E next() {
    // 检查是否被其它迭代器或ArrayList自己修改
    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];
}  
// 检查是否被其它线程修改的函数
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

  next 方法是实际用来迭代的方法,返回迭代的元素,并且让指针指向下一个元素。首先检查是否被并发修改,这里检查的方法就是比较 modCount 和 expectedModCount是否相等。expectedModCount 是每个迭代器私有的变量,初始化为ArrayList对象的modCount,所以如果如果在进行迭代时发现这两个值不一样,会直接抛出异常。在上一篇博客里,每次调用诸如add,remove这样的函数,都会修改modCount值。Java官方给的注释为:

The number of times this list has been <i>structurally modified</i>.
* Structural modifications are those that change the size of the
* list, or otherwise perturb it in such a fashion that iterations in
* progress may yield incorrect results.  

  只有在进行所谓的‘结构性修改’才会改变modCount的值,‘结构性修改’就是改变list的size的操作,或者其它可能会在迭代过程中造成错误结果的操作(这个我也没想到是什么操作),简单的set是不会修改modCount的值的,这一点在上一篇博客里可以看到。

  并发修改又怎么样?我想了个例子,比方说list对象现在是[1,2,3,4,5],正常咱们一直调用next迭代,希望得到一个1-5的序列。如果没有并发检查,你迭代到3的时候,其它线程把1和2删了,那么现在size变成了3,也不用再迭代了,最终得到了一个1-3的序列。这到底是个啥呢?这个list最终是[3,4,5],迭代要么返回最新的3-5,要么返回之前的1-5,返回一个1-3这有什么意义呢?这只是我自己想的一个例子。不难看出,如果没有并发检查,会造成很多错误,比如值的错误,越界错误等。

  接着进行if (i >= size)判断是否迭代完了,在取得值之前,还会进行if (i >= elementData.length)判断,如果判断为真,会抛出并发修改异常。这个判断我感觉应该是走到这个判断,说明之前if (i >= size)这个判断是过了的,i < size但是现在又发现i >= elementData.length,说明两个代码之间list又被删了很多元素。之后就是cursor+1,lastRet设置为之前的cursor值,取到结果。

  不过我觉得这也不能确保就一定安全,比方说在checkForComodification() 函数调用之后,next()函数取得值之前,list又新增了很多值,这在后面的检查里是看不出来的。

remove方法

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

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        // 设置expectedModCount,防止之后抛出异常
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

  首先检查lastRet是否小于0,lastRet小于0,要么是还没开始迭代时,为-1,要么是已经调用过一次remove,会被再次设置为-1(后面代码就是)。这也可以发现,没办法连续两次调用remove。接着检查并发修改。之后调用ArrayList对象自己的remove方法来进行删除,然后更新cursor和lastRet的值:cursor设置为lastRet,也就是自减了1,由于ArrayList的remove会把删除元素后面的元素都往前移一位,所以cursor对应的元素仍然没变。最后会设置迭代器的expectedModCount,因为ArrayList的remove会修改modCount值。所以我们发现,如果在用迭代器迭代元素时,想删除元素,可以调用迭代器的remove方法,这不会导致后面的迭代抛出异常;如果调用ArrayList的remove,会导致expectedModCount和modCount值不一致,迭代器就无法再使用了。

  还可以发现,迭代器删除元素也不过是调用ArrayList自己的remove罢了,迭代器本身可以做的事情真的很少。

forEachRemaining

public void forEachRemaining(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int size = ArrayList.this.size;
    int i = cursor;
    if (i < size) {
        final Object[] es = elementData;
        if (i >= es.length)
            throw new ConcurrentModificationException();
        for (; i < size && modCount == expectedModCount; i++)
            action.accept(elementAt(es, i));
        // update once at end to reduce heap write traffic
        cursor = i;
        lastRet = i - 1;
        checkForComodification();
    }
}

  前面还是一些检查并发修改,下标越界的代码,主要看这行for (; i < size && modCount == expectedModCount; i++),i在被初始化为cursor,从循环条件可以得知,此循环从cursor开始,到size-1结束,对此范围的元素进行操作。进行的操作就是action.accept(elementAt(es, i));,这个action的声明为Consumer<? super E> action,Consumer是Java提供给我们的函数式接口,里面只有一个抽象方法:accept。我们可以这样调用 forEachRemaining :iter.forEachRemaining(System.out::println);,这样就可以快速的打印iter里剩下的元素,其实是用方法引用来代替实现了Consumer接口的类的对象,也可以自行编写lambda表达式,当然,也可以定义一个Consumer接口的实现类。

  可以看出,每次循环时,都会判断modCount == expectedModCount来进行并发修改检查,如果此判断为假,结束循环,在这种情况下,最后的checkForComodification();肯定也是抛出异常。

ListIterator

  Iterator的方法确实比较少,ArrayList还有个内部类,ListItr,提供了更多的方法。

初始化和内部属性

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

public ListIterator<E> listIterator() {
    return new ListItr(0);
}  
// ListItr类
private class ListItr extends Itr implements ListIterator<E> {
    ListItr(int index) {
        super();
        // 从指定index开始迭代
        cursor = index;
    }
}

  可以看出,ListIterator可以从指定的Index开始迭代,它并没有新的属性。

previous方法

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];
}

  可以看出,就是返回迭代器cursor指向元素的前一个元素,调用此函数会导致cursor回退一位,lastRet和cursor一样。

set方法

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

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

  进行set时,set的是lastRet对应的Index,调用时需保证lastRet > 0,所以如果初始化时或者调用迭代器的remove之后调用set,是无法成功的。

add方法

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

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

  调用ArrayList自己的add 之后,会让cursor自增一个,由于add 会把index之后的元素都向后挪一位,所以cursor还是指向它之前指向的元素,最后设置expectedModCount。

subList

  Java的subList代码上的第一句注释为:Returns a view of the portion of this list,返回此列表的部分视图,下面来看看这个'view'。

初始化和内部结构

// 创建subList
public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList<>(this, fromIndex, toIndex);
}  

private static class SubList<E> extends AbstractList<E> implements RandomAccess {
    private final ArrayList<E> root;
    private final SubList<E> parent;
    private final int offset;
    private int size;

    // 从ArrayList创建subList
    public SubList(ArrayList<E> root, int fromIndex, int toIndex) {
        this.root = root;
        this.parent = null;
        this.offset = fromIndex;
        this.size = toIndex - fromIndex;
        this.modCount = root.modCount;
    }

    // 从subList再创建subList
    private SubList(SubList<E> parent, int fromIndex, int toIndex) {
        this.root = parent.root;
        this.parent = parent;
        this.offset = parent.offset + fromIndex;
        this.size = toIndex - fromIndex;
        this.modCount = parent.modCount;
    }

  由于subList继承了AbstractList类,所以它自己也有一个modCount属性;可以选择由ArrayList或者subList来创建一个新的subList。首先看由ArrayList初始化subList,毕竟你需要有第一个subList,才能用subList初始化出其它的subList。可以看到,这里把root 设置为这个ArrayList对象本身,parent设置为null,然后分别设置offset,size和modCount。

  再看由subList初始化subList,可以发现,root还是那个root,不管你subList嵌套了多少层,parent就是此subList上面一层,offset就是此subList相对于原始ArrayList的偏移量,层层叠加,size就是subList的长度,modCount和parent的保持一致。用一张自己手画的图来表示这个初始化过程:

7bd74aab24b826192d35790d17d0e77.jpg

  下面看几个具体函数。

get方法

public E get(int index) {
    Objects.checkIndex(index, size);
    checkForComodification();
    return root.elementData(offset + index);
} 

// 检查并发性修改
private void checkForComodification() {
    if (root.modCount != modCount)
        throw new ConcurrentModificationException();
}

  这个方法比较简单,先检查自己的modCount和原对象的modCount,最后访问元素,靠的是offset + index作为索引的下标,这也正是我上面说的,offset是偏移量。

add方法

public void add(int index, E element) {
    rangeCheckForAdd(index);
    checkForComodification();
    root.add(offset + index, element);
    updateSizeAndModCount(1);
}

  方法还是靠调用 root 也就是原始的ArrayList 对象的方法来操作,注意到最后调用了一个updateSizeAndModCount(1);,看一下这个方法:

private void updateSizeAndModCount(int sizeChange) {
    SubList<E> slist = this;
    // 改变此层及以上层的size和modCount
    do {
        slist.size += sizeChange;
        slist.modCount = root.modCount;
        slist = slist.parent;
    } while (slist != null);
}

  递归的改变这层和以上层的size和modCount,从这也可以发现,此层以下的subList就不管了,所以如果subList嵌套了许多层,需要用subList进行结构性修改的话,最好用最下面那层来改,不然,下面的subList就都废掉了。

其它方法

  还有很多方法,诸如removeRange,addAll,removeAll,retainAll等等,代码都是调用原ArrayList对象的对应方法,用 offset 作为开始,调用完成再用updateSizeAndModCount();来进行此层和上层subList的更新。

迭代器

  subList也可以有自己的迭代器,o(≧口≦)o 这个我真不想写了,代码都差不多。感觉这个subList设计真的挺复杂,理论上讲,可以由subList层层嵌套得到一个subList,再由此subList得到一个迭代器,感觉如果这么写代码的话,debug都很困难。

for-each循环

  说到迭代器,还得看看for-each循环。看这样一个代码:

for(Integer item:list) {
    System.out.println(item);
}

  这里面的初始化我就省略了,就是用for-each循环进行遍历并输出。我通过在IDEA里打断点,进行debug模式,捕捉到了代码的运行情况。

image.png

image.png

image.png

  可以发现,ArrayList的for-each循环,其实就是新建了一个迭代器,不断进行hasNext()next()的调用。

总结

  对于ArrayList来说,不管是迭代器还是subList,它们本身并不存储什么数据,只是用一些诸如cursor, lastRet, expectedModCount, offset 等变量作为索引下标的记录,通过增删改查函数的编写,提供一种迭代,遍历的机制。