前言
接着我的上一篇博客 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的保持一致。用一张自己手画的图来表示这个初始化过程:
下面看几个具体函数。
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模式,捕捉到了代码的运行情况。
可以发现,ArrayList的for-each循环,其实就是新建了一个迭代器,不断进行hasNext()
和next()
的调用。
总结
对于ArrayList来说,不管是迭代器还是subList,它们本身并不存储什么数据,只是用一些诸如cursor, lastRet, expectedModCount, offset 等变量作为索引下标的记录,通过增删改查函数的编写,提供一种迭代,遍历的机制。