ArrayList简介
ArrayList是一个实现了List接口的动态数组,其容量能够动态增长,其允许包括null在内的所有元素。它继承了AbstractList,实现了List、RandomAccess, Cloneable, java.io.Serializable。
因为其基于数组实现的特性,所以随机访问 get 和 set 效率较高,但是在于添加和删除操作,由于要移动数据,因此效率会低些。
需要注意的是,ArrayList中的操作不是线程安全的!如果多线程同时访问 ArrayList ,而其中有个线程对表结构做了修改,则它可能会抛出错误。所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList。下面我们就了解一下ArrayList一些常用的方法属性,以及一些需要注意的地方。
本文转自ArrayList源码分析(基于JDK8),本人仅在自己的理解上做了些许的修改。
ArrayList属性
属性中比较重要的两个就是用于记录当前数组长度的 size,以及元素的缓存数组 elementData,除此之外还有一个经常用到的属性就是从AbstractList继承过来的modCount属性,代表ArrayList集合的修改次数。
1 | public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable { |
ArrayList 构造方法
无参构造方法
如果不传入参数,则使用默认无参构建方法创建ArrayList对象,如下:
1 | public ArrayList() { |
注意:此时我们创建的ArrayList对象中的elementData中的长度是1,size是0,当进行第一次add的时候,elementData将会变成默认的长度:10.
带int类型的构造函数
传入的参数用于指定初始数组长度,若参数大于等于0,则给elementData 赋予对应长度的Object数组,
若参数小于0,则抛出IllegalArgumentException异常,构造方法如下:
1 | public ArrayList(int initialCapacity) { |
带Collection对象的构造函数
构造一个包含指定 Collection 的元素的列表,这些元素是按照该 collection 的迭代器返回它们的顺序排列的。
其内部实现大致为:
- 将 Collection 对象转为数组,然后将数组的地址的赋给elementData。
- 更新size的值,同时判断size的大小,如果是size等于0,直接将空对象EMPTY_ELEMENTDATA的地址赋给elementData
- 如果size的值大于0,且 elementData 的 class 不等于 Object 数组的 class,则执行Arrays.copyOf方法,把 Collection对象的内容(可以理解为深拷贝)copy到 elementData 中。
注意:this.elementData = arg0.toArray(); 这里执行的简单赋值时浅拷贝,所以要执行Arrays,copy 做深拷贝
1 | public ArrayList(Collection<? extends E> c) { |
常用方法
add 方法
add(E e) 方法
此方法将指定的元素添加到列表的尾部中,内部实现逻辑如下:
- 确保数组已使用长度(size)加1之后足够存入下一个数据
- 修改次数modCount 标识自增1,如果当前数组已使用长度(size)加1后的大于当前的数组长度,则调用grow方法,增长数组,grow方法会将当前数组的长度变为原来容量的1.5倍。
- 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上,size自增1
- 成功返回 true
添加元素方法入口:
1 | public boolean add(E e) { |
确保添加的元素有地方存储:
1 | private static int calculateCapacity(Object[] elementData, int minCapacity) { |
将修改次数(modCount)自增1,判断是否需要扩充数组长度,判断条件就是用当前所需的数组最小长度与数组的长度对比,如果大于0,则增长数组长度。
1 | private void ensureExplicitCapacity(int minCapacity) { |
如果当前的数组已使用空间(size)加1之后大于数组长度,则增大数组容量,扩大为原来的1.5倍。
1 | private void grow(int minCapacity) { |
add(int index, E element) 方法
这个方法其实和上面的add类似,该方法可以按照元素的位置,指定位置插入元素,其后的元素往后移动一位,具体的执行逻辑如下:
- 越界检查,否则抛出异常
- 确保数组已使用长度(size)加1之后足够存入下一个数据
- 修改次数(modCount)标识自增1,如果当前数组已使用长度(size)加1后的大于当前的数组长度,则调用grow方法,增长数组
- grow方法会将当前数组的长度变为原来容量的1.5倍。
- 确保有足够的容量之后,使用System.arraycopy 将需要插入的位置(index)后面的元素统统往后移动一位。
- 将新的数据内容存放到数组的指定位置(index)上
1 | public void add(int index, E element) { |
注意:使用该方法的话将导致指定位置后面的数组元素全部重新移动,即往后移动一位。
get方法
返回此列表中指定位置上的元素
1 | public E get(int index) { |
set方法
1 | public E set(int index, E element) { |
contains方法
直接调用了 indexOf 方法,若返回小于 0 则返回 false。
1 | public boolean contains(Object o) { |
1 | /** |
remove方法
根据索引remove
- 越界检查,否则抛出异常
- modCount 自增1
- 将指定位置(index)上的元素保存到oldValue
- 将指定位置(index)上的元素都往前移动一位
- 将最后面的一个元素置空,好让垃圾回收器回收
- 将原来的值oldValue返回
1 | /** |
注意:调用这个方法不会缩减数组的长度,只是将最后一个数组元素置空而已。
根据对象remove
循环遍历所有对象,得到对象所在索引位置,然后调用fastRemove方法,执行remove操作
1 | /** |
定位到需要remove的元素索引,先将index后面的元素往前面移动一位(调用System.arraycooy实现),然后将最后一个元素置空。
1 | private void fastRemove(int index) { |
clear方法
modCount 自增,当前数组长度内的所有元素赋为 null,并把 size 置 0
1 | /** |
sublist方法
我们看到代码中是创建了一个ArrayList 类里面的一个内部类SubList对象,传入的值中第一个参数时this参数,其实可以理解为返回当前list的部分视图,真实指向的存放数据内容的地方还是同一个地方,如果修改了sublist返回的内容的话,那么原来的list也会变动。
1 | public List<E> subList(int fromIndex, int toIndex) { |
trimToSize方法
- 修改次数加1
- 将elementData中空余的空间(包括null值)去除,例如:数组长度为10,其中只有前三个元素有值,其他为空,那么调用该方法之后,数组的长度变为3。Arrays.copyOf返回的是一个新数组,因此达到了节省空间的效果
1 | public void trimToSize() { |
iterator方法
interator方法返回的是一个内部类,由于内部类的创建默认含有外部的this指针,所以这个内部类可以调用到外部类的属性。
1 | public Iterator<E> iterator() { |
Iterator 的 next方法
一般的话,调用完iterator之后,我们会使用iterator做遍历,这里使用next做遍历的时候有个需要注意的地方,就是调用next的时候,可能会引发ConcurrentModificationException,当修改次数,与期望的修改次数(调用iterator方法时候的修改次数)不一致的时候,会发生该异常,详细我们看一下代码实现:
1 | "unchecked") ( |
expectedModCount这个值是在用户调用ArrayList的iterator方法时候确定的,但是在这之后用户add,或者remove了ArrayList的元素,那么modCount就会改变,那么这个值就会不相等,将会引发ConcurrentModificationException异常,这个是在多线程使用情况下,比较常见的一个异常。
1 | final void checkForComodification() { |
Iterator 的 remove方法
cursor 记录了当前元素索引,lastRet则记录了上一元素的索引,调用迭代器自带的remove方法,先调用了ArrayList的remove方法后,
会把操作次数modCount赋给expectedModCount,因此不会引发异常。
1 | public void remove() { |
System.arraycopy 方法
参数 | 说明 |
---|---|
src | 原数组 |
srcPos | 原数组 |
dest | 目标数组 |
destPos | 目标数组的起始位置 |
length | 要复制的数组元素的数目 |
Arrays.copyOf方法
original - 要复制的数组
newLength - 要返回的副本的长度
newType - 要返回的副本的类型
其实Arrays.copyOf方法返回的是一个全新数组,其底层也是调用System.arraycopy实现的,源码如下:
1 | public static int[] copyOf(int[] original, int newLength) { |
小结
ArrayList总体来说比较简单,不过ArrayList还有以下一些特点:
- ArrayList自己实现了序列化和反序列化的方法,因为它自己实现了 private void writeObject(java.io.ObjectOutputStream s)和 private void readObject(java.io.ObjectInputStream s) 方法
- ArrayList基于数组方式实现,无容量的限制(会扩容)添加元素时可能要扩容(所以最好预判一下),删除元素时不会减少容量(若希望减少容量,trimToSize()),删除元素时,将删除掉的位置元素置为null,下次gc就会回收这些元素所占的内存空间 。
- 线程不安全
- add(int index, E element):添加元素到数组中指定位置的时候,需要将该位置及其后边所有的元素都整块向后复制一位
- get(int index):获取指定位置上的元素时,可以通过索引直接获取(O(1))
- remove(Object o)需要遍历数组
- remove(int index)不需要遍历数组,只需判断index是否符合条件即可,效率比remove(Object o)高
- contains(E)需要遍历数组
- 使用iterator遍历可能会引发多线程异常