这篇博客主要是用来对ArrayList的源码解析,相信大家在工作中对ArrayList的使用应该是非常多的,下面我将详细分析他们源码,看能否帮大家查漏补缺,作者使用的IDE是IntelliJ,jdk版本是1.8,建议读者也用相同的环境打开源码跟着一起分析,下面正式进入主题:
首先我们先看下ArrayList的类关系图:
从图中可以看出,ArrayList主要是继承了AbstractList抽象类,其余的实现类我们讲方法的时候讲到了会带过,下面我们简单看下这个类里面的几个方法:
// add 方法
public boolean add(E e) {
add(size(), e);
return true;
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
// get 方法
abstract public E get(int index);
// set 方法
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
// remove 方法
public E remove(int index) {
throw new UnsupportedOperationException();
}
从上面的代码段可以看出来,AbstractList是提供了一个类似于模板模式的一些方法,并且有些方法是直接抛错的,强制需要继承类自己实现这一点和AQS是一样的设计思想(实际上java.util下面的很多都是这样的设计思想,多看我两篇文章就知道啦),既然大概了解了他的父类方法,我们就来看看ArrayList是怎么实现的吧!
首先我们看下ArrayList的几个属性
// 序列化版本号(因为实现了java.io.Serializable)
private static final long serialVersionUID = 8683452581122892189L;
// 默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认容量空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存储元素的数组(不可序列化)
transient Object[] elementData; // non-private to simplify nested class access
// 当前ArrayList的元素个数
private int size;
接着我们来看下ArrayList的构造方法:
// 1、无参构造器,初始化存储元素的数组为默认容量空数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 2、 带容量参数的构造器,主要是用来指定大小的ArrayList的初始化
public ArrayList(int initialCapacity) {
// 传入参数大于0时初始化一个同等大小的Object数组赋值给elementData
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 传入参数等于0时使用空数组赋值
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 传入参数小于0时直接抛错
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 3、传入Collection集合初始化ArrayList
public ArrayList(Collection<? extends E> c) {
// 调用toArray()方法(见下方),将其转换为object()数组
elementData = c.toArray();
// 如果传入的集合不为空走if逻辑,else就会初始化一个空数组
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
// 解决bug问题,由于其c.toArray()可能出现返回值不为Object[]的错误。
// 因为使用toArray的方法有所不同,向上转型赋值可能会报错,所以采用如下方法,该bug在jdk1.9里面修复了
if (elementData.getClass() != Object[].class)
//使用数组拷贝来进行
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
好啦,构造方法讲完了,那我们就开始讲一下我们常用的API吧,首先讲add(E element)方法
// add 方法,非线程安全,因为肯那个存在同一个size被多个线程抢到去add的时候会产生数据丢失的情况
public boolean add(E e) {
// 判断容量是否满足,size前面讲属性的时候初始化是0的
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
// 我们一步步分析他是如果判断容量是否满足和扩容的
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 返回最大容量 第一次无参构造的话返回的是 10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
// 这里使用了一个Fail-Fast 机制 ,快速失败机制,主要是非线程安全的类使用的一种快速报错的手段
modCount++;
// 判断现在的容量是否还能容纳新增的这个元素,第一次一般都是会走grow,因为我们一般使用的都是无参构造
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
// grow 方法是用来做扩容
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 将容量扩展为现在的1.5倍 oldCapacity + (oldCapacity >> 1) “>>1” 是右移一位就是除以2
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 扩容之后的新容量如果还小于minCapacity则将minCapacity赋值给他,第一次无参构造的add进来时赋值的10
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 当扩容长度超过了最大长度限制则返回Integer.MAX_VALUE
newCapacity = hugeCapacity(minCapacity);
// 这里调用Arrays.copyOf点进去看的话会发现是调用的native方法,那么问题来了native在jvm内存模型里是在哪个模块呢?
elementData = Arrays.copyOf(elementData, newCapacity);
// 好了,我们回到add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将value e放进elementData[size]中,然后size = size+1
elementData[size++] = e;
return true;
}
}
add方法讲完了,我们可以看下相关联的add(int index, E element) ,这个方法和add(E element) 区别不大,主要是多了个数组越界检查,和指定index赋值:
public void add(int index, E element) {
// 数组越界检查,这里为啥要用size做检查而不用elementData.length做检查呢?
// 我猜想是因为数组在内存里是连续空间,新增需要对其他元素后移,如果是新增的不是<=size+1的位置,那么就会出现断层
rangeCheckForAdd(index);
// 检查是否需要扩容
ensureCapacityInternal(size + 1); // Increments modCount!!
// native 方法扩容,需要从index开始重新后移
// 这里需要注意 add(int index, E element)会后移Index及index以后的所有数据至新的index以后
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
// 真正替换在这里
elementData[index] = element;
size++;
}
还有个 addAll(Collection<? extends E> c)这个方法,这个方法也很简单
public boolean addAll(Collection<? extends E> c) {
// 转为object数组
Object[] a = c.toArray();
int numNew = a.length;
// 扩容
ensureCapacityInternal(size + numNew); // Increments modCount
// 直接不管c.toArray()这个bug,使用通用的native方法赋值
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
下面讲讲get方法,比较简单
public E get(int index) {
// 数组越界检查,这里用的>=size 跟上面add方法的检查不一样,为什么?因为add是可以新增一个的而这个是访问所以不能越界
rangeCheck(index);
// 返回index数据
return elementData(index);
}
好了,到了常用方法的最后一个了remove方法
// 根据下标删除
public E remove(int index) {
// 数据越界检查,同上
rangeCheck(index);
// 快速失败
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
// 这里是判断是否需要前移,>0 证明删除的index不是最后一个,所以需要前移(因为数组是连续的)
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // 手动置为null,方便gc
return oldValue;
}
// 根据对象删除
public boolean remove(Object o) {
// 遍历,然后调用快速删除
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
// 跟上面的下标删除不一样的是,快速删除没有判断数组越界,因为这里已经有判断了
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
然后我们讲一讲前面提到的fail-fast modCount这个属性吧,这里主要用在常用的场景里,比如说sort,removeIf,forEach,只要被改变就会报错 ConcurrentModificationException并发修改异常。
好啦,今天的源码解析就到这里了,我们来总结下 ArrayList的特点吧:
1、ArrayList内部是Object[]数组,然后使用的时候进行动态扩容。ArrayList默认会分配10个元素的数组,然后在此基础上进行扩容,每次新的扩容后的数组长度是原数组长度的1.5倍。如果你大概知道集合的容量,可以指定初始化值,减少扩容带来性能损耗。
2、查找和修改性能强大,数组查找连续内存,速度快。删除操作相对较慢,绝大部分需要移动数组
3、非线程安全,在多线程里面使用需要加锁或者使用线程安全的容器比如CopyOnWriteArrayList