Java集合之ArrayList源码分析
简介
ArrayList是Java中比较常用的一个集合,从单词拼写可以理解它是一个数组的列表,通常具有以下的特性:
- 元素允许为null
- 允许重复数据,占用的是数组中不同位置的地址
- 数据有序,即与添加元素的顺序相同
- 非线程安全
源码解析
ArrayList为何具有上面的特点以及它适合什么场景中进行使用,下面从源码来分析一下原理(无特别说明时源码皆是jdk1.7):
基本属性及构造方法
上面是基本的几个属性,注释中都说的比较清楚了,DAFAULT_CAPACITY是默认初始化的数组大小,EMPTY_ELEMENTDATA是一个用来判断用的空数组,elementData是用来存放数据的数组,size则是记录元素的数量。
普通构造方法指定容量大小,非小于0时,正常初始化创建一个目标容量大小的Object数组并赋给elementData属性。未指定目标容量大小则使用默认大小。另外一种特殊的构造方法是通过传入一个目标集合构造出新的ArrayList,构造方法如下:
此处参数使用了泛型的类型通配符上限方式定义,限定了元素类型上限,对泛型不清楚的请点泛型解析回顾一下。这里的elementData和size都取目标集合转为数组的相关属性。检查elementData的元素类型如若非Object[]类型则使用数组拷贝的方式对elementData进行赋值。
添加元素
首先进行添加元素,给原来的size+1然后进行大小判断操作,如果最后得到的minCapacity大于当前的elementData长度,则需要进行扩容操作,进入grow方法,源码如下:
扩容操作中首先记录当前容量大小记录为old,定义新大小 为old的1.5倍(>>1 右移1位,即除以2),判断如果定义的新容量依然小于所需容量则使用所需容量大小作为新容量,接下来对大小的上限作判断最大为Integer的MAX_VALUE即为2^31-1,接下来继续使用数组的拷贝方法将老数组的数据全部拷贝到新数组中,完成扩容操作。 回到原add方法中,将新添加的元素放入数组尾端,完成添加元素操作。
除正常的添加元素方法外,还支持指定元素在数组中的位置插入,具体源码如下:
rangeCheckForAdd方法是确保目的的index合法,ensureCapacityInternal方法上面已经解析过了,作用是确保数组容量如不足够则进行扩容,
接下来进行数组元素操作,由于是插入操作,这里将目标index及其后的元素全部往后移动一位腾出位置为element提供位置,这里使用的是System.arraycopy方法,与扩容时使用的Array.copyof的作用不同,这里介绍一下区别:
- arraycopy()是将原数组拷贝到目标数组中(可以是同一个数组),而且可以选择拷贝的起点和长度以及放入新数组中的位置;
- copyOf()是系统自动在内部新建一个数组,调用arraycopy()将original内容复制到copy中去,并且长度为newLength。返回copy即将原数组拷贝到一个长度为newLength的新数组中,并返回该数组。
回到方法中,接下来将目标element放入数组的index位置,size自增。元素插入操作结束。
删除元素
按照下标删除:
rangeCheck是确保index合法的方法。取出即将移除的目标元素用来提供方法返回, 判断size-index-1是否大于0 ,也就是判断index是否在尾部,不在尾部的话将index+1及其后的元素全部向前移动一位,将老元素覆盖,后将数组的末尾元素置为null方便虚拟机触发gc,size自减。返回被删除的目标元素。根据下标删除元素操作完成
根据元素删除:
传入目标元素,非空与null原理都相同:对数组元素进行遍历,如果在数组中找到与目标相同的元素的,根据下标进行删除,操作与上面的根据下标删除元素相同并且返回true,没有找到则返回false。
注:数组中实际存放的是元素的地址并非是具体的元素数据,结合JVM内存模型可知,此处方便故而直接描述。
ArrayList特点总结及扩展
ArrayList优缺点及使用场景
优点:
- ArrayList采用数组结构实现,支持元素的随机访问,实现了RandomAccess接口(注:此为空接口,是非典型的接口用法,实现的作用是为了再此集合进行元素查找或操作时会对是否实现RandomAccess而采用不同的算法,例如二分查找等),在元素查找方面速度很快;
- 可能大家平时听说ArrayList不适用于常有元素添加的场合,其实这句话从上面的添加元素源码看来并不完全正确,因为在只往末尾添加元素的情况下,ArrayList的效率还是很高的,只需要在数组末尾添加元素即可。
缺点:
- ArrayList的缺点主要是因为在进行元素插入或者删除的时候需要进行元素复制,在插入时如果需要进行扩容,那么就需要对老数组的元素复制到新数组中,容量越大则复制消耗的开销就越大;在删除时,同样如果移动时复制的元素越多开销就越大。
综合所述,ArrayList适合支持随机访问支持下标访问和顺序添加的场合使用。
ArrayList在多线程下的问题
由于ArrayList的方法都未进添加同步机制,故而在多线程使用时会出现线程不安全问题。一种方法是使用Collections中的 同步集合系列方法中的synchronizedList方法,使其变为线程安全。另一种方式是使用ArrayList的线程安全版本Vector,Vector大部分与ArrayList相同,较大的差别是Vector有自己的增长因子用来扩容时,扩容时代码如下
其他基本相同,并且通过jdk几个版本的优化,目前的Vector效率与ArrayList相差很小,适合在线程安全情况下使用,具体选择依据自身条件即可。
ArrayList的elementData是transient的原因
在上面结构源码中我们看到在定义elementData时加上了transient关键字,此关键字的作用是防止此属性序列化。ArrayList实现了Serializable接口(注:此接口与RandomAccess类型,是空接口,是用作标记使用,表明实现该接口的类可进行序列化操作)却不想最主要的属性elementData被序列化,原因是什么呢?我们细想一下可知,在进行序列化的操作时候elementData数组可能元素还未达到数组的容量大小,对其属性进行序列化显然会浪费多余的空间与开销,故而ArrayList重写了writeObject方法和readObject方法来进行序列化相关操作,只序列化存在的元素,提高了序列化的速度与开销。