ArrayList源码学习(二)

280 阅读6分钟

本文上接ArrayList源码学习(一),将继续介绍ArrayList中的剩余部分,类中还包括一些私有内部类用于实现上层接口规定的迭代器iterator和子列表subList

迭代器

按照List接口规范,列表支持两种迭代器:从集合继承来的普通迭代器和列表自身的允许双向遍历的迭代器。ArrayList类中使用class Itrclass ListItr自定义了两种迭代器的具体实现,也是对AbstractList中实现的优化。

Iterator

// 以适当的顺序返回此列表中元素的迭代器
public Iterator<E> iterator() {
    return new Itr();
}

ArrayList通过上面方法返回该列表的迭代器对象,然后通过迭代器对象就能对列表进行遍历。

成员变量&构造器

private class Itr implements Iterator<E> {
    int cursor;       // 下一个将要返回的元素的索引
    int lastRet = -1; // 上一个返回元素的索引,如果通过调用 remove 删除此元素,则重置为 -1
    int expectedModCount = modCount; // 迭代器认为支持列表应该具有的 modCount 值。 如果违反此期望,则迭代器已检测到并发修改

    Itr() {}
}

变量expectedModCount初始化为列表结构被修改的次数,在迭代器遍历过程中如果列表结构被修改,则快速失败中断遍历,将在下文源码中详细介绍。

迭代器操作

// 如果迭代器还有元素遍历,则返回true
public boolean hasNext() {
    return cursor != size;
}

该方法判断是否迭代器是否遍历结束,此处使用游标cursor和列表长度比较,如果遍历结束将会使cursor == size

// 返回迭代的下一个元素
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];
}
// 校验并发修改,使用final定义,防止被重写,比较安全
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

next()是迭代器遍历列表的主要方法,首先就是执行贯穿整个迭代器的一个逻辑:校验并发修改,通过列表的modCount和迭代器创建时的expectedModCount比较,如果不相等就说明迭代器生成以后列表结构已经被修改,就要抛出ConcurrentModificationException中断遍历,这就是迭代器快速失败的主要逻辑。校验并发修改以后就进入正常的逻辑:

  1. 检查迭代器的游标值,如果大于列表size,就表示遍历结束,没有将要返回的值,抛出NoSuchElementException
  2. 如果游标值大于列表数组的长度,则说明此时的列表的结构被修改过,抛出并发修改异常ConcurrentModificationException
  3. 最后将cursor递增,等于下一个元素的索引,并且返回当前元素,使lastRet赋值当前元素的索引。
// 从列表中移除迭代器返回的上一个元素,每次调用next()只能调用一次本方法
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();
    }
}

首先检查lastRet的值,正常遍历情况下,该值等于上一个返回元素的索引。如果小于0,说明过去没有调用过next()返回元素(或者已经执行过本方法),因此也没有元素可删除,抛出非法状态异常IllegalStateException,接着执行了检查并发修改的逻辑。

正常校验通过以后,调用外部类的remove()删除列表指定索引的元素。然后将lastRet的值赋给cursor,因为删除一个元素下一个要遍历的元素索引仍为当前索引,按照ArstractList文档规范,迭代器删除元素后,要将lastRet重置为 -1,这也说明了第一步产生lastRet < 0的另一种情况。最后一步不能忘掉更新expectedModCount为新的modCount,毕竟删除元素也会使列表结构修改,这样迭代器才能正常遍历下去。

而在此代码中,为了进一步防止并发修改,捕捉了所有IndexOutOfBoundsException,将抛出并发修改异常。

该迭代器中包含了另外一个遍历方法forEachRemaining用来一次性遍历剩余元素,代码逻辑比较清晰,在这不做源码解析。

ListIterator

// 返回此列表中元素的列表迭代器,从列表中的指定位置开始。 指定的索引指示初始调用next将
// 返回的第一个元素。 对previous的初始调用将返回指定索引减一的元素
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);
}

以上是ArrayList中两种获取列表迭代器的方法,该列表迭代器是从上文普通迭代器继承而来,并实现了ListIterator的接口,因此向后遍历的逻辑和Itr中一样,新增了向前遍历的逻辑和一些其他方法。

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

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 boolean hasPrevious() {
    return cursor != 0;
}

向前遍历的方法和next()的实现逻辑类似,只是将游标cursor向前“滑动”。而当游标到0的时候就不能再进行previous遍历了。此外,还提供了两个获取前后元素索引的方法:

// 返回后续调用next将返回的元素的索引。如果列表迭代器位于列表末尾,则返回列表大小
public int nextIndex() {
    return cursor;
}
// 返回后续调用previous将返回的元素的索引。如果列表迭代器位于列表的开头,则返回 -1
public int previousIndex() {
    return cursor - 1;
}

其他方法

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

根据文档,该方法将元素游标cursor之前,对后续调用next()无影响,而调用previous()就会返回该元素。因此方法内,插入以后会将游标加 1,lastRet重置为 -1,并且更新expectedModCount的值。

// 将next或previous返回的最后一个元素替换为指定的元素(可选操作),
// 仅当在最后一次调用next或previous之后没有调用remove和add才能进行此调用
public void set(E e) {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();
    try {
        ArrayList.this.set(lastRet, e);
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

方法禁止在调用remove()add()后调用,因此首先就检查了lastRet,如果被重置为 -1,就表示非法的状态。然后再检查并发修改,最后直接调用ArrayList的set()方法替换元素。

ArrayList中支持的迭代器操作比较简单,却非常方便的控制着列表的遍历访问操作,快速失败机制也非常严谨的控制着列表的修改。

子列表

子列表subList是列表List独有的操作性质,返回的子列表由父列表所支持,因此对子列表的非结构性修改会反映在父列表中,反之亦然。(结构性修改是那些改变这个列表的大小,或者以其他方式扰乱它,以致正在进行的迭代可能会产生不正确的结果)。在ArrayList中,子列表有内部类SubList实现,继承了AbstractList实现了列表基本的逻辑操作和迭代器操作。

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

static void subListRangeCheck(int fromIndex, int toIndex, int size) {
    if (fromIndex < 0)
        throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
    if (toIndex > size)
        throw new IndexOutOfBoundsException("toIndex = " + toIndex);
    if (fromIndex > toIndex)
        throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                           ") > toIndex(" + toIndex + ")");
}

该方法是列表中唯一获取子列表的方法,通过参数两个索引获取在这个区间内元素组成的子列表。必要的,需要检查参数的有效性:起始索引不能小于 0,结束索引不能大于列表长度,并且起始索引不能比结束索引大。根据文档规定,如果fromIndex和toIndex相等,则返回的列表为空。然后通过参数构造一个子列表。

SubList

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

    SubList(AbstractList<E> parent,
            int offset, int fromIndex, int toIndex) {
        this.parent = parent;
        this.parentOffset = fromIndex;
        this.offset = offset + fromIndex;
        this.size = toIndex - fromIndex;
        this.modCount = ArrayList.this.modCount;
    }
}

其内部维护了一个父列表的AbstractList类型引用,parentOffset表示相较于父列表的偏移量,即等于子列表的 fromIndex,操作parent时会使用该值;而 offset 另外记录一个表示相较于顶级列表的偏移量,当使用子列表创建子列表的时候将会设置该值,否则等于 fromIndex;size则记录了该子列表的长度,此外还记录了modCount控制并发操作。

SubList的操作方法逻辑和ArrayList中基本相同,只不过操作的列表数组是顶级父列表中的数组,这里只介绍部分方法用于区分。

public E get(int index) {
    rangeCheck(index);
    checkForComodification();
    return ArrayList.this.elementData(offset + index);
}

public void add(int index, E e) {
    rangeCheckForAdd(index);
    checkForComodification();
    parent.add(parentOffset + index, e);
    this.modCount = parent.modCount;
    this.size++;
}

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

get方法获取该子列表中的元素,校验索引和并发修改外的方法已经比较熟悉,而取值则是通过offset + index去ArrayList的数组中取。在add方法中,添加元素又通过持有的父列表对象添加。最后subList方法中创建子列表传入了偏移量offset,这是和ArrayList.subList()的唯一差别。通过这些能很好的看出parentOffsetoffset两个变量的作用。

原文地址 --- Java容器源码学习分析专题——ArrayList源码学习(二)