本文基于JDK-8u源码分析
1简介
ArrayList作为最基础的集合类,其底层是使用一个动态数组来实现的,这里“动态”的意思是可以动态扩容(虽然ArrayList可以动态扩容,但却不会动态缩容)。但是与HashMap不同的是,ArrayList使用的是1.5的扩容策略,而HashMap使用的是2的方式。还有一点与HashMap不同:ArrayList的默认初始容量为10,而HashMap为16。
有意思的一点是:在Java7之前的版本中,ArrayList的无参构造器是在构造器阶段完成的初始化;而从Java7开始,改为了在add方法中完成初始化,也就是所谓的延迟初始化。在HashMap中也有同样的设计思路。
另外,同HashMap一样,如果要存入一个很大的数据量并且事先知道要存入的这个数据量的固定值时,就可以往构造器里传入这个初始容量,以此来避免以后的频繁扩容。
2构造器
3add方法
3.1add(Ee)
添加指定的元素:
3.2add(intindex,Eelement)
在指定的位置处添加指定的元素:
4get方法
5remove方法
5.1remove(Objecto)
删除指定的元素:
5.2remove(intindex)
删除指定位置处的元素:
6不要在foreach循环里进行元素的remove/add操作
这是《阿里巴巴编码规范》中的一条。正例:
反例:
运行上面的代码可以看到,使用迭代器的删除操作是不会有问题、能成功删除的;而使用foreach循环进行删除则会抛出ConcurrentModificationException异常,但如果使用foreach循环删除第一个元素“1”的时候又会发现不会抛出异常。那么这到底是为什么呢?
众所周知,foreach循环是一种语法糖,那么其背后到底是如何来实现的呢?将上面反例的代码反编译后的结果如下:
上面的内容不需要完全看懂,只需要看到第23行代码处、第26行代码处和第29行代码处后面的解释,也就是foreach循环是通过调用List.iterator方法来生成一个迭代器,通过Iterator.hasNext方法和Iterator.next方法来实现的遍历操作(普通的for循环不是通过这种方式,也就是说普通的for循环不会有这种问题)。
那么首先来看一下ArrayList中iterator方法的实现:
而抛出异常是在上面第17行代码处的checkForComodification方法里面抛出的,下面来看一下它的实现:
可以看到如果modCount和expectedModCount不等就会抛出ConcurrentModificationException异常。而上面说过,在add方法和remove方法中,会对modCount修改标志位做+1的操作。这里的modCount是为了做快速失败用的。**快速失败指的是如果在遇到并发修改时,迭代器会快速地抛出异常,而不是在将来某个不确定的时间点冒着任意而又不确定行为的风险来进行操作,也就是将可能出现的bug点推前。**在包括HashMap在内的绝大部分集合类都是有快速失败机制的。注意:这里的并发修改指的并不都是发生在并发时的修改,也有可能是在单线程中所做的修改导致的,就如同上面的反例一样。
这里拿上面的反例来举例,ArrayList调用了两次add方法,也就是此时的modCount应该为2。而expectedModCount如上所示,一开始会初始化为modCount的值,也就是也为2。
6.1remove操作
首先来看一下删除“2”的情况:
第一次循环:
因为此时的modCount和expectedModCount都为2,所以第一次循环中不会抛出异常,抛出异常都是发生在不是第一次循环的情况中。在next方法走完后,foreach循环方法体中的remove方法的if条件判断不满足,就结束了本次循环。
第二次循环:
第二次循环的hasNext和next方法都是能成功走完的,在这之后会进入到foreach循环方法体中的remove方法中,进行删除元素。而此时的size-1变为了1。上面分析过,在remove方法中的fastRemove方法中,会对modCount+1,也就变为了3。
第三次循环:
然后会走入到第三次循环中的hasNext方法中。按照正常的情况下该方法是会返回false的,但因为此时的size已经变为了1,而此时的cursor为2(cursor代表下一次的索引位置),所以两者不等,错误地返回了true,所以会继续走入到next方法中的checkForComodification方法中,判断此时的modCount和expectedModCount是否相等。因为此时的modCount已经变为了3,和expectedModCount的值为2不等,所以在此抛出了ConcurrentModificationException异常。
再来看一下删除“1”的时候为什么不会抛出异常:
第一次循环:
同上,此时的modCount和expectedModCount都为2,所以第一次循环中的hasNext和next方法都不会抛异常。在这之后会进入到foreach循环方法体中的remove方法中,进行删除元素。同上,size-1变为了1,而modCount+1变为了3。
第二次循环:
在第二次循环的hasNext方法中,此时的cursor为1,而size也是1,两者相等。所以hasNext方法返回false,就跳出了foreach循环,不会走到随后的next方法中,也就不会抛出异常。
6.2add操作
然后来看一下add操作的情况,其实在第一次循环中添加元素和不是第一次循环中添加元素、从而抛出异常的原因是类似的,这里就以第一次循环中添加元素来举例:
第一次循环:
同上,此时的modCount和expectedModCount都为2,所以第一次循环中的hasNext和next方法都不会抛异常。在这之后会进入到foreach循环方法体中的add方法中,进行添加元素。size+1变为了3,而modCount+1也变为了3。
第二次循环:
在第二次循环的hasNext方法中,此时的cursor为1,而size为3,两者不等,所以hasNext方法返回true,会走到随后的next方法中。而在next方法中的checkForComodification方法中,此时的modCount已经变为了3,而expectedModCount还是为2。两者不等,所以在此抛出了ConcurrentModificationException异常。
其实从上面的几次分析中就可以看出:只要在foreach循环方法体中有进行修改过modCount和size的操作,就都有可能会是抛出异常的。
6.3迭代器操作
6.3.1remove操作
既然现在已经知道了foreach循环中使用remove/add操作抛出异常的原因,那么就可以分析一下为什么使用迭代器进行相关操作就不会有问题呢?上面正例代码中的第5行代码处的iterator方法、第6行和第7行代码处的hasNext和next方法都是跟foreach循环里的实现是一样的,而区别在于第9行代码处的remove操作。这里的remove不是ArrayList中的remove操作,而是Itr内部类中的remove操作:
可以看到第7行代码处是调用了ArrayList的remove操作进行删除的,但同时注意第10行代码处会将expectedModCount更新为此时modCount的最新值,这样在next方法中就不会抛出异常了;在第8行代码处会将cursor更新为lastRet(lastRet代表上一次的索引位置),即将cursor-1(因为此时要remove,所以cursor指针需要减一)。这样在hasNext方法中就会返回正确的值了。
6.3.2add操作
虽然iterator方法可以提供remove操作来使删除能正确执行,但其却没有提供相关add方法的API。无妨, ArrayList中为我们提供了listIterator方法,其中就有add操作(如果一定要用迭代器方式来实现的话)。相关的示例代码如下:
listIterator方法返回了一个ListItr内部类。那么就来看一下ListItr的代码实现:
可以看到ListItr内部类是继承了Itr类,包括hasNext和next等方法都是直接复用的。而在add方法中的第14行代码处,是调用了ArrayList的add操作进行添加的。另外和Itr的remove方法一样,第17行代码处也是在更新expectedModCount为此时modCount的最新值,第15行代码处的cursor更新为+1后的结果(因为此时是在做add操作)。这样后续的hasNext和next操作就不会有问题了。