ArrayList的基本特点
- 快速随机访问
- 允许存放多个null元素
- 底层是Object数组
- 增加元素个数可能很慢(可能需要扩容),删除元素可能很慢(可能需要移动很多元素),改对应索引元素比较快
ArrayList的继承关系
来看下源码中的定义
1 | public class ArrayList<E> extends AbstractList<E> |
- 可以看到继承了AbstractList,此类提供 List 接口的骨干实现,以最大限度地减少实现”随机访问”数据存储(如数组)支持的该接口所需的工作.对于连续的访问数据(如链表),应优先使用 AbstractSequentialList,而不是此类.
- 实现了List接口,意味着ArrayList元素是有序的,可以重复的,可以有null元素的集合.
- 实现了RandomAccess接口标识着其支持随机快速访问,实际上,我们查看RandomAccess源码可以看到,其实里面什么都没有定义.因为ArrayList底层是数组,那么随机快速访问是理所当然的,访问速度O(1).
- 实现了Cloneable接口,标识着可以它可以被复制.注意,ArrayList里面的clone()复制其实是浅复制(不知道此概念的赶快去查资料,这知识点非常重要).
- 实现了Serializable 标识着集合可被序列化。
ArrayList 的构造方法
在说构造方法之前我们要先看下与构造参数有关的几个全局变量:
1 | /** |
注意到,底层容器数组的前面有一个transient关键字,啥意思??
查阅资料后,大概知道:transient标识之后是不被序列化的
但是ArrayList实际容器就是这个数组为什么标记为不序列化??那岂不是反序列化时会丢失原来的数据?
其实是ArrayList在序列化的时候会调用writeObject(),直接将size和element写入ObjectOutputStream;反序列化时调用readObject(),从ObjectInputStream获取size和element,再恢复到elementData。
原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。
无参构造方法
1 | /** |
命名里面讲elementData指向了一个空数组,为什么注释却说初始容量为10。这里先卖个关子,稍后分析。
指定初始容量的构造方法
1 | public ArrayList(int initialCapacity) { |
如果我们预先知道一个集合元素的容纳的个数的时候推荐使用这个构造方法,避免使用ArrayList默认的扩容机制而带来额外的开销.
使用另一个集合 Collection 的构造方法
1 | /** |
增加元素+扩容机制
添加单个元素
add(E e)
方法作用: 添加指定元素到末尾
1 | /** |
大体思路:
首先判断如果新添加一个元素是否会导致数组溢出
判断是否溢出:如果原数组是空的,那么第一次添加元素时会给数组一个默认大小10.接着是判断是否溢出,如果溢出则去扩容,扩容规则: 新数组大小是原来数组大小的1.5倍,最后通过Arrays.copyOf()去浅复制.
添加元素到末尾
2. 添加元素到指定位置
add(int index, E element)
方法作用:添加元素到指定位置
1 | /** |
大体思路:
这里理解了上面的扩容之后,这里是比较简单的.其实就是在数组的某一个位置插入元素,那么我们将该索引处往后移动一位,腾出一个坑,最后将该元素放到此索引处(填坑)就行啦.
添加集合到末尾
addAll(Collection
1 | public boolean addAll(Collection<? extends E> c) { |
大体思路:
代码思路是非常清晰的,很简单,就是将需要插入的集合转成数组a,再将a数组插入到当前elementData的末尾(其中还判断了一下是否需要扩容).
添加集合到指定位置
addAll(int index, Collection
1 | public boolean addAll(int index, Collection<? extends E> c) { |
大体思路:
其实就是一个先挖坑,再填坑的故事.首先判断一下添加了集合之后是否需要扩容,因为需要将集合插入到index处,所以需要将index后面的元素往后挪动,需要挪动的元素个数为:size - index,挪动的间隔是index + numNew(因为需要留出一个坑,用来存放需要插入的集合).
有了上面的步骤后就可以安全的将集合复制到elementData的index,也就完成了集合的插入.
其实我们可以看到,源码中对于细节的处理很细致,值得学习.
删除元素
移除指定位置元素
remove(int index)方法作用:移除指定位置元素,可能会抛出IndexOutOfBoundsException或ArrayIndexOutOfBoundsException
1 | public E remove(int index) { |
大体思路:
- 首先将旧值取出来,保存起来
- 然后将数组的index后面的元素往前挪动一位
- 将数组的末尾元素赋值为null,方便GC回收.因为已经将index后面的元素往前挪动了一位,所以最后一位是多余的,及时清理掉.
移除指定元素
remove(Object o)方法作用:移除指定元素,只移除第一个集合中与指定元素相同(通过equals()判断)的元素.移除成功了则返回true,未移除任何元素则返回false
- 如果传入的是null,则移除第一个null元素
- 如果传入的是非null元素,则移除第一个相同的元素,通过equals()进行比较.所以,如果是自己写的类,则需要重写equals()方法.一般需要用到元素比较的,都需要实现equals()方法,有时候还需要重写hashCode()方法.
1 | public boolean remove(Object o) { |
大体思路:
- 首先判断需要移除的元素是否为null
- 如果为null,则循环遍历数组,移除第一个为null的元素
- 如果非null,则循环遍历数组,移除第一个与指定元素相同(equals() 返回true)的元素
可以看到最后都是移除指定位置的元素,源码中为了追求最佳的性能,加了一个fastRemove(int index)方法,次方法的实现与remove(int index)是几乎是一样的,就是少了返回index索引处元素的值.
从此列表中删除所有包含在给定集合中的元素
removeAll(Collection
1 | public boolean removeAll(Collection<?> c) { |
大体思路:
- 首先我们进行c集合检查,判断是否为null
- 然后我们调用batchRemove()方法去移除 c集合与当前列表的交集
- 循环遍历当前数组,记录c集合中没有的元素,放在前面(记录下标为w),w前面的是留下来的元素,w后面的是需要删除的数据
- 第3步可能会出错,出错的情况下,则将出错位置的后面的全部保留下来,不删除
- 然后就是将w之后的元素全部置空(方便GC回收),然后将size(标记当前数组有效元素)的值赋值为w,即完成了删除工作
再笼统一点说吧,其实就是将当前数组(elementData)中未包含在c中的元素,全部放在elementData数组的最前面,假设为w个,最后再统一置空后面的元素,并且记录当前数组有效元素个数为w.即完成了删除工作.
清空列表
clear() 方法作用:清空当前集合的所有元素
这个方法非常简单,就是将数组所有元素都置为null,然后GC就有机会去把它回收了
1 | public void clear() { |
移除相应区间内的所有元素(protected)
removeRange(int fromIndex, int toIndex)方法作用:移除指定区间内的所有元素,注意这是protected方法,既然是移除元素,那么就拿出来欣赏欣赏.
1 | //这是protected方法 移除相应区间内的所有元素 |
大体思路:
- 假设需要移除(fromIndex,toIndex)区间内的元素,那么将toIndex后面的元素复制到fromIndex处
- 将有效元素后面的元素置空
改动元素
替换指定下标的元素内容
set(int index, E element):替换index索引处的元素为element,可能会抛出IndexOutOfBoundsException
这里比较简单,就是将index处的元素替换成element
1 | public E set(int index, E element) { |
查询元素
返回指定位置处元素
这个非常简单,就是将index索引处的数组的值返回
1 | E elementData(int index) { |
通过iterator()遍历
这也是查询的一种,哈哈
首先我们了解一下fail-fast,fail-fast 机制是java集合(Collection)中的一种错误机制。
当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。例如:当某一个线程A通过iterator去遍历某集合的过程中,
若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。
要了解fail-fast机制,我们首先要对ConcurrentModificationException 异常有所了解。当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。同时需要注意的是,该异常不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出该异常。
我们先来看看iterator()方法,它new了一个Itr(ArrayList的内部类)进行返回.
1 | /** |
接下来我们来看看这个内部类
1 | private class Itr implements Iterator<E> { |
总结
这是我第二次看源码,分析,鉴赏,学到了不少东西,相信各位认真看完的同学也多多少少有些感触.源码对于细节方面想的很周到,很谨慎.
下面我们来总结一下ArrayList的关键点
ArrayList关键点
- 底层是Object数组存储数据
- 扩容机制:默认大小是10,扩容是扩容到之前的1.5倍的大小,每次扩容都是将原数组的数据复制进新数组中. 我的领悟:如果是已经知道了需要创建多少个元素,那么尽量用
new ArrayList<>(13)
这种明确容量的方式创建ArrayList.避免不必要的浪费. - 添加:如果是添加到数组的指定位置,那么可能会挪动大量的数组元素,并且可能会触发扩容机制;如果是添加到末尾的话,那么只可能触发扩容机制.
- 删除:如果是删除数组指定位置的元素,那么可能会挪动大量的数组元素;如果是删除末尾元素的话,那么代价是最小的. ArrayList里面的删除元素,其实是将该元素置为null.
- 查询和改某个位置的元素是非常快的( O(1) ).