Java集合框架源码分析之ArrayList

开篇寄语: 我们唯一有的是时间,成功就取决于我们怎么利用时间和它的副产品——闲暇时间。


总述: ArrayList是一个允许重复元素的集合类,内部通过数组来存储元素。


1. 类声明:

很明显,ArrayList实现了四个接口,如果你打开这四个接口的话,你会发现除了List中定义了方法,其他三个接口都是空接口,没有任何方法定义。其实,这也是一种接口的使用方式,用空接口来做标志,表明某项特性或约束。

|  RandomAccess表明ArrayList是可以随机访问的,也就是说ArrayList可以通过下标来访问

|  Cloneable表明ArrayList是可以克隆的,这就意味着ArrayList对象是可以调用Object中的clone方法的(虽然clone方法是定义在Object中,但是只有实现Cloneable接口的类对象可以调用,否则会报CloneNotSupportedException);

|  Serializable表明ArrayList是可被序列化的。

下面,来看看ArrayList的一些核心方法。


2. 构造器

既然ArrayList内部是用数组来实现元素存储的,那么构造器必然需要对数组成员对象实例化。通过源代码,你会发现无参构造器的默认数组初始大小是10(这个经常有面试官会问,面试官其实是想知道你是否看过ArrayList源码,从而判断你对Java集合框架的使用深度,所以请记住吧)。


3. 添加元素

如果你看看add的系列方法,从源码中你会发现,凡是add系列方法,第一步都是会调用ensureCapacity方法,这个方法是用于实现ArrayList的可扩展数组能力的关键方法,它会计算比较当前元素数+1(即将被插入的元素)后的数组元素个数和数组长度,如果前者大于后者,则意味着需要扩展当前数组大小,从源代码中可以看出,这个时候会将数组大小扩展为插入前数组大小的1.5倍。


4. 删除元素

如果,您足够细心和好奇的话,会对add/remove系列方法中对于成员变量modCount的操作感到疑惑。甚至在ensureCapacity方法中,也会对先modCount做++操作,那么modCount是用来做什么的呢?其实这个modCount是用于并发控制的。让我们设想一种场景:当一个线程对一个ArrayList对象进行迭代操作时,另外一个线程在对该ArrayList对象做add操作,那么这个时候新增的元素是不能被迭代器可见的,你可能会想不可见就不可见呗,应该也不会出什么大问题吧?的确是这样,这其实也是后续博客会介绍的CopyOnWriteList设计思想,只保证元素的最终可见性,而不保证该可见性的实时性。那么,如果另一个线程在对该ArrayList进行remove操作呢,这个时候可能会发生什么呢?让我们想象极端情况,如果迭代器操作到了最后一个元素,而且其hasNext方法刚刚执行完毕,这个时候线程上下文切换,导致了该元素被remove掉,继续上下文切换,执行迭代器的next方法,这个时候很明显会发生数组下标越界的异常。为了尽早的发现这种并发导致的异常,在创建ArrayList迭代器时,会保存该modCount的一个副本,如果在迭代过程中发现该副本的值和当前modCount值不相同,则表明存在并发修改ArrayList大小的操作发生,抛出ConcurrentModificationException。那么,现在可以明白,ArrayList用抛出异常的手段既保证了元素的可见性,又保证了这种可见性的实时性,当然这也了牺牲并发性。在实际应用中,如果只需要保证元素可见性,而不需要精确的保证实时性,则可以使用CopyOnWriteList,来享受并发带来的效率提升。典型的应用场景有监听器和过滤器列表的维护,如MINA框架中就是这样做的。


5. 克隆方法

从方法实现可以看出,只是对ArrayList对象本身进行了深度克隆,并没有对底层元素也做深度克隆;如果要完整的深度克隆,则要保证其中的所有元素对象都要实现Clonable接口,并且覆盖实现克隆方法。


6. 迭代器

集合框架中的迭代器都是以成员内部类方式存在的。注意checkForComodification方法,modCount的作用就在这个方法中体现。


7. 时间复杂度

|  插入指定元素的时间复杂度很明显是O(1);插入指定索引位置的时间复杂度为O(N),主要是要移动元素位置。

|  删除操作的时间复杂度为O(N),因为删除后都需要移动元素。

|  查询元素的时间复杂度为O(1),因为可以根据索引随机访问元素。

你可能感兴趣的:(java,ArrayList)