本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
前言
上篇文章介绍了ArrayList底层的数据结构以及常见的增删改查方法,详见# JDK8 ArrayList源码解析P1。今天来看一下最常用但却没怎么听说过的部分,迭代器Iterator。
迭代器 Iterator
在使用ArrayList时我们经常需要遍历集合来完成某些操作,通常有两种方式,一种是直接使用foreach,一种是使用Iterator接口,但两种本质上是一样的,foreach底层实现还是Iterator接口。
Itr
Itr是ArryList中的一个私有内部类,实现了Iterator接口用以实现遍历。
private class Itr implements Iterator<E> {
...
}
成员变量
// 迭代过程中,下一个元素的位置,默认从 0 开始
int cursor;
// 新增场景:表示上一次迭代过程中,索引的位置;删除时置为 -1
int lastRet = -1;
// protected transient int modCount = 0;
// modCount是ArrayList的一个属性字段,表示数组实际的版本号
// expectedModCount 表示迭代过程中,期望的版本号;
int expectedModCount = modCount;
主要方法
迭代器主要就三个方法:
hasNext:是否还有下一个元素;next:下一个元素值;remove:删除当前迭代的值;
hasNext()
public boolean hasNext() {
// cursor 表示下一个元素的位置,size 表示实际大小
// 如果两者相等,说明已经没有元素可以迭代了,
// 如果不等,说明还可以进行迭代
return cursor != size;
}
next()
@SuppressWarnings("unchecked")
public E next() {
// 迭代过程中,判断版本号有无被修改
// 如果被修改,抛 ConcurrentModificationException 异常
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;
// 返回元素值 同时将lastRet设置为 i
return (E) elementData[lastRet = i];
}
next方法做了两件事:
- 是否还能继续迭代;
- 定位到当前迭代的值,并为下一次迭代做好准备;
checkForComodification()
// 检测 ArrayList 中的 modCount 和当前迭代器对象的 expectedModCount 是否一致
// 不等的话直接抛出异常
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
这个方法没啥好说的,就是检查ArrayList中的modCount和当前迭代器对象的 expectedModCount是否一致,不一样就抛出ConcurrentModificationException。
remove()
public void remove() {
// 如果上一次操作时,数组的位置已经小于 0 了,说明数组已经被删除完了
if (lastRet < 0)
throw new IllegalStateException();
// 迭代过程中,判断版本号有无被修改,
// 如果被修改,抛 ConcurrentModificationException 异常
checkForComodification();
try {
// 此处调用的是ArrayList的 remove(int index)
ArrayList.this.remove(lastRet);
// 设置下一个位置
cursor = lastRet;
// -1 表示元素已经被删除,这里也防止重复删除
lastRet = -1;
// 删除元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount
// 这样下次迭代时,两者的值是一致的了
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
需要注意的点是:
lastRet= -1 的操作目的,是防止重复删除操作;- 删除元素成功,数组当前
modCount就会发生变化,这里会把expectedModCount更新为modCount的值,下次迭代时两者的值就会一致了;
ListItr
ListItr是Itr的子类,除了基本的三个方法之外,还额外实现了一些其他的方法。我们都知道,在遍历集合时,若是对集合结构进行修改则会触发fast-fail机制,但若是我们的确有这个需求,一边遍历一边修改集合的结构,那怎么办呢?答案就在ListItr中的add方法中。
add(E e)
public void add(E e) {
//迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
checkForComodification();
try {
//获取当前遍历到的位置
int i = cursor;
//插入到数组中
ArrayList.this.add(i, e);
//再向下移动一个位置 因而本次遍历过程中访问不到这个元素 只能在下一次遍历中才能访问
cursor = i + 1;
//更新lastRet
lastRet = -1;
// 添加元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount
// 这样下次迭代时,两者的值是一致的了
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
使用add这个方法在遍历时添加时,本次遍历是访问不到添加的那个元素的,只能在下一次遍历时才能访问,保证了本次遍历的正确性,从而防止出现fast-fail。
其他方法
ListItr还实现了ListIterator这个接口,因而还可以前向遍历,这里只给出源码,简单看一下就行。
//判断是否还有前一个元素
public boolean hasPrevious() {
return cursor != 0;
}
//下个元素的索引
public int nextIndex() {
return cursor;
}
//前一个元素的索引
public int previousIndex() {
return cursor - 1;
}
//迭代时,前一个元素的值
@SuppressWarnings("unchecked")
public E previous() {
//校验modCount
checkForComodification();
//前一个位置索引
int i = cursor - 1;
if (i < 0)//参数校验
throw new NoSuchElementException();
//实际存储数据的数组
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)//参数校验
throw new ConcurrentModificationException();
//更新cursor
cursor = i;
//返回当前迭代的值,并设置 lastRet
return (E) elementData[lastRet = i];
}
总结
结合JDK8 ArrayList源码解析P1和本篇文章一起总结一下ArrayList的特点,如下:
- 底层是动态数组,默认大小为10,扩容时每次都是当前数组长度的1.5倍;
- 使用无参构造或者
capacity=0的有参构造时,ELEMENTDATA都是空数组,只有在第一次添加元素时才去扩容; - 扩容主要方法为
grow(int minCapacity),不支持缩容; - 允许元素为
null; - 实现
RandomAccess接口,支持随机访问,平均时间复杂度为O(1); - 底层是数组,所以增加和删除元素过程时,效率低下,平均时间复杂度为
O(n);