之前写过一篇ArrayList源码的博客 https://www.cnblogs.com/zumengjie/p/13538394.html 其中遗留了一个问题,ArrayList添加元素和删除元素或者清空元素时都会有一个操作 modCount++;当时并没有将死磕到底的精神进行到底。这两天在一本源码书籍里边提到了这个变量,看完后决定记录下来。这本书我也介绍大家去读《Java修炼指南-高频源码解析》是开课吧组编的书,这本书四个字形容,开卷有益。
下文带//为个人注释。源代码使用这个颜色
迭代集合引发的问题
Itr迭代器
ListItr迭代器继承自Itr
ListItr的特色功能
Fail-Fast机制
一些有趣的方法
迭代集合引发的问题
首先先看一段代码。这段代码就是在for循环中使用集合本身的remove方法删除元素。结果大家也都知道因为删除了元素,size减去1,变量i又自增1。最终导致被元素后边的一个没有被遍历到。B被删除了,C顶替了B它的index是2但是变量i已经是3了所以跳过。
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.add("E");
// 结果是 A D E
for (int i = 0; i < list.size(); i++) {
if (i == 1) {
list.remove(i);
} else {
System.out.println(list.get(i));
}
}
接着再看这一段代码。看这段代码主要就是要知道在迭代器中使用集合本身的删除,新增都会报错。ConcurrentModificationException。
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.add("E");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if (next.equals("B")) {
list.remove("B");// 报错 java.util.ConcurrentModificationException
list.remove(1);// 报错 java.util.ConcurrentModificationException
list.add("F");// 报错 java.util.ConcurrentModificationException
} else {
System.out.println(next);
}
}
但是可以使用迭代器自己的新增或者删除元素的方法。
ArrayList<String> list = new ArrayList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
list.add("E");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if (next.equals("B")) {
iterator.remove();
} else {
System.out.println(next);
}
}
Itr迭代器
//它返回的是一个Iterator对象是ArrayList中的一个内部类,Iterator<E> iterator()其实是List接口的一个抽象方法。
public Iterator<E> iterator() {
return new Itr();
}
int cursor;//指向下一个元素的下标,默认是0。
int lastRet = -1;//上一个被迭代的下标。
int expectedModCount = modCount;//这个变量在集合长度变化时会自增1
public boolean hasNext() {
return cursor != size;//判断有没有下一个元素的依据是cursor的size!=size,cursor默认是0。如果size是3,cursor也是3那就没有下一个元素了。
}
//在迭代器里抛出的异常就是它做的,当迭代器创建后expectedModCount==modCount,但如果我们使用集合本身的方法改变了集合的长度modCount就会增加而expectedModCount不会增加。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
@SuppressWarnings("unchecked")
public E next() {//获取集合内的下一个元素
checkForComodification();//进来后就走了判断,如果通过迭代器以外的方法修改了集合的长度,那这个方法就直接异常了。
int i = cursor;//默认是0
if (i >= size)//这里也是健壮性的判断,和hasNext()起到同样的效果。
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;//拿到ArrayList中的数组。
if (i >= elementData.length)//健壮性的判断
throw new ConcurrentModificationException();
cursor = i + 1;//cursor+1,这表示下一个被读取的元素的下标。
return (E) elementData[lastRet = i];//获取当前元素,并且把下标赋值给lastRet,也就是说这个变量的意思是上一个被迭代的下标。
}
public void remove() {//删除当前被迭代的元素
if (lastRet < 0)
throw new IllegalStateException();//lastRet默认是-1,也就是说如果没有调用next()就调用remove()是会报错的。
checkForComodification();//检验modCount
try {
ArrayList.this.remove(lastRet);//此处调用了ArrayList自身的remove(index)
cursor = lastRet;//这里有点意思。假如调用next()获取的元素是第四个,其实cursor是5而lastRet是4,这里把lastRet赋值给cursor意味着下次next()返回的结果还是第四个。
lastRet = -1;//这里将lastRet置为-1表示如果连续调用两个remove()也会报错。
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//这个方法其实就是用来遍历集合的,不要试图使用这个方法时改变集合长度,否则极大的几率会出现异常。
//另外这个方法中使用的cursor,lastRet和迭代器中其他方法是共用的。
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);//断言判断
final int size = ArrayList.this.size;//当前集合的长度
int i = cursor;//cursor默认是0
if (i >= size) {//健壮性判断
return;
}
final Object[] elementData = ArrayList.this.elementData;//拿到数组
if (i >= elementData.length) {//健壮判断
throw new ConcurrentModificationException();
}
//这里就是遍历集合元素,把当前元素交给参数consumer消费。
//如果你在消费时使用集合本身的增加或者删除方法,其效果和普通for循环一样。
//如果你在消费时使用迭代器的remove(i)则会直接报错,因为lastRet默认是-1。
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
//这里修改cursor意味着再次调用next将会返回false。
cursor = i;
lastRet = i - 1;
//这里会再次检验modCount
checkForComodification();
}
ListItr迭代器继承自Itr
public ListIterator<E> listIterator() {
return new ListItr(0);//它创建的是带参数的 Listltr(0)
}
ListItr(int index) {
super();
//传入的index赋值给了cursor,这意味着我们可以手动的指定下一次迭代的位置。
//但是需要注意cursor更改后,lastRet没有跟着更改,意味着创建listIterator调用迭代器的remove()同样会异常。
cursor = index;
}
//获取迭代器下一次迭代的下标。
public int nextIndex() {
return cursor;
}
//在Itr类中是没有add()方法的。
public void add(E e) {
checkForComodification();//进来后首先做的就是检验modCount的值。
try {
int i = cursor;//当前下标赋值变量
//调用ArrayList自己的添加元素方法。将元素添加到当前i的位置。
//如果当前next元素是1,那么next后就是2也就是在当前迭代元素后添加元素。
ArrayList.this.add(i, e);
//迭代下标+1,这个操作意味着,新添加的元素不会被迭代到。
//例如当前集合为[A,B,C,D,E]当前next指向的是B,则next后cursor就是2
//在2位置添加元素后就是[A,B,N,C,D,E],那其实下一个next就是N但是cursor+1后下一个就是C了。
cursor = i + 1;
lastRet = -1;//添加后同样会lastRet同样是-1意味着同时调用两次add(e)也会报错。
expectedModCount = modCount;//将modCount赋值给expectedModCount。
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
//这个set方法,倒是有些多余了,因为ArrayList集合本身的set方法不会更改modCount
//因为ListItr的set方法还会检验modCount的值。
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();//检验modCount
try {
ArrayList.this.set(lastRet, e);//ArrayList自身的set()方法。
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
ListItr的特色功能
//判断有没有上一个,默认的cursor是0,使用了next()后cursor+1。意味着有上一个元素。
public boolean hasPrevious() {
return cursor != 0;
}
//如果有上一个则调用previous获取上一个元素。
@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();//先检验modCount
int i = cursor - 1;//当前cursor左移
if (i < 0)//如果当前迭代器的cursor已经在最左边了,就要报错。
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;//拿到集合中的数组
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];//这里取得是cursor-1
}
//或者可以使用previousIndex向左移动cursor,但是总感觉这个方法使用不当会很危险。
public int previousIndex() {
return cursor - 1;
}
Fail-Fast机制
以上内容解释了modCount这个变量到底是干啥用的。但是都没有用很专业很有B格的名词唠一下。
Fail-Fast是Java中一种错误检测机制!它的触发机制是,在迭代过程中如果集合的结构发生了
变化此时迭代器并不知情或没有来得及反应就会产生Fail-Fast事件。抛出ConcurrentModificationException异常。
所以modCount和迭代器中的expectedModCount不相等就会报错这就是Fail-Fast机制。
一些有趣的方法
forEach
//这个方法是JDK8出的,为了结合拉姆达表达式使用。
//它接收一个Consumer消费者接口,这个接口只有一个accept方法。
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;//获取modCount
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;//获取数组
final int size = this.size;//获取集合长度
for (int i=0; modCount == expectedModCount && i < size; i++) {//循环数组
action.accept(elementData[i]);//将数组的每个元素交给Consumer的accept方法并且执行。
}
if (modCount != expectedModCount) {//此处也会判断如果modCount出现改变会报错。
throw new ConcurrentModificationException();
}
}
trimToSize
//这个方法用来收缩数组,当已经确认了集合内不会添加元素时,调用此方法将集合内
//的数组进行收缩,也就是说将数组内的元素拷贝到新的数组中,新的数组大小和size相同
public void trimToSize() {
//这个变量现在已经很熟悉了吧。
modCount++;
if (size < elementData.length) {
//如果size是0就把空数组赋值给elementData,如果不是就做数组拷贝。
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
subList
看下图的执行结果截取后的小集合元素为BC,小集合添加一个元素后卫BCF。令人迷惑的是大集合的元素ABCFDE。
首先subList(int,int)是对集合做截取返回的一个List但是注意,返回的集合并不是ArrayList而是SubList。
public List<E> subList(int fromIndex, int toIndex) {
//这个方法是对入参进行限定起始位置不能小于0,结束位置不能大于大集合的size,其实位置不能大于结束位置。否则都会抛出异常。
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
//这是ArrayList中的一个内部类。它继承自AbstractList也就是说它和ArrayList是兄弟关系。
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
//这个变量和offset是一致的,要截取数组的起始位置是N那么小集合和大集合的偏移量就是N
//也就是说要获取小集合的M下标就是要到大集合中找N+M位置的元素
private final int parentOffset;
private final int offset;
int size;
//以上图为例构造函数参数分别是当前ArrayList,0,1,3
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;//这个值是1
this.offset = offset + fromIndex;//这个值是0+1
this.size = toIndex - fromIndex;//这个值是2
this.modCount = ArrayList.this.modCount;//ArrayList里的集合操作数变量。到处都有它。
//这是AbstractList中的add方法我们调用的是这个。而它则调用自己的双参数方法也就是调用了子类自己的。
public boolean add(E e) {
add(size(), e);
return true;
}
//这是SubList里的add方法.传入的参数是自己的size也就是2
public void add(int index, E e) {
//这里进行了检验index不能小于0不能大于size
rangeCheckForAdd(index);
//这个方法依然检验的是modCount如果当前SubList的modCount和ArrayList的modCount不一样也会抛出异常。
checkForComodification();
//这里调用的是ArrayList的add(int,object)传入的第一个参数是1+2=3也就是在ArrayList集合的第三个下标处添加元素。
//也就是说添加的元素始终是在SubList的最后一个,最终的结果ABCFDE
parent.add(parentOffset + index, e);
//将ArrayList的modCount赋值给SubList的modCount
this.modCount = parent.modCount;
this.size++;
}
//获取元素
public E get(int index) {
rangeCheck(index);
checkForComodification();
//要获取的下标+偏移量
return ArrayList.this.elementData(offset + index);
} }}
为什么说增强for就是迭代器?
最后再回到forEach这个方法。SubList中本身是没有这个方法的,它来自于Iterable继承体系是
SubList->AbstractList->AbstractCollection->Collection->Iterable
这是一个默认实现的方法。这里看似很简单,无非就是循环当前对象,然后把当前元素值传递
给Consumer.accept(o)但是这么想就错了。因为当前这个this其实是SubList但是SubList里并没有
Object[]也就是它根本就没有装在元素的数组。
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
断点后的截图如下
走到这我就好奇了,这个this到底是怎么生成的。于是我想使用反编译工具JD-JUI看看。但是失败了。这个工具如果有拉姆达表达式就会失败。
于是我打算曲线救国,既然这个forEach使用的增强for那么我就看看增强for这个语法糖到底干了什么?
不得不喷一句这个工具太垃圾了!于是我又找到了JDK自带的工具javap
终于这就是我要的答案!它走的是迭代器!!!SubList也有自己的迭代器。这里只看next就能看出来,每次查询下一个其实就是使用了offset偏移量+index就像是get方法一样。到此结束!不过有空我还要去找一个好点
的反编译器彻底的反编译一把这个代码。
Arrays.asList
//这个方法不是ArrayList中的方法它是Arrays工具包里的方法。
//之所以聊到它是因为这个方法接收可变长度的数组然后返回
//一个ArrayList但是这个ArrayList可不是咱们本文的ArrayList,
//而是Arrays工具类中的内部类。
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
看一个有意思的代码片段。
这段代码执行是会报错的,原因就是虽然返回的也是ArrayList但是这个ArrayList只是继承了AbstractList而AbstractList类中的add方法本身
的add方法是这个样子的。
public void add(int index, E element) {
throw new UnsupportedOperationException();
}