本篇分析ArrayList的源码,在分析之前先跟大家谈一谈数组。数组可能是我们最早接触到的数据结构之一,它是在内存中划分出一块连续的地址空间用来进行元素的存储,由于它直接操作内存,所以数组的性能要比集合类更好一些,这是使用数组的一大优势。
但是我们知道数组存在致命的缺陷,就是在初始化时必须指定数组大小,并且在后续操作中不能再更改数组的大小。在实际情况中我们遇到更多的是一开始并不知道要存放多少元素,而是希望容器能够自动的扩展它自身的容量以便能够存放更多的元素。
ArrayList就能够很好的满足这样的需求,它能够自动扩展大小以适应存储元素的不断增加。它的底层是基于数组实现的,因此它具有数组的一些特点,例如查找修改快而插入删除慢。本篇我们将深入源码看看它是怎样对数组进行封装的。
1、类的继承
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
2、类的属性
// 版本号
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;
// 实际元素大小,默认为0
private int size;
// 最大数组容量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
说明:类的属性中核心的属性为elementData,类型为Object[],用于存放实际元素,并且被标记为transient,也就意味着在序列化的时候,此字段是不会被序列化的。但是ArrayList类中重写了readObject和writeObject方法,所以其实是有序列化的。
3、构造函数
3.1、无参构造函数
public ArrayList() {
// 无参构造函数,设置元素数组为空
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
说明:当未指定初始化大小时,会给elementData赋值为空集合。
3.2、ArrayList(int)型构造函数
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) { // 初始容量大于0
this.elementData = new Object[initialCapacity]; // 初始化元素数组
} else if (initialCapacity == 0) { // 初始容量为0
this.elementData = EMPTY_ELEMENTDATA; // 为空对象数组
} else { // 初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
3.3、集合参数构造函数
public ArrayList(Collection<? extends E> c) { // 集合参数构造函数
elementData = c.toArray(); // 转化为数组
if ((size = elementData.length) != 0) { // 参数为非空集合
if (elementData.getClass() != Object[].class) // 是否成功转化为Object类型数组
elementData = Arrays.copyOf(elementData, size, Object[].class); // 不为Object数组的话就进行复制
} else { // 集合大小为空,则设置元素数组为空
this.elementData = EMPTY_ELEMENTDATA;
}
}
说明:当传递的参数为集合类型时,会把集合类型转化为数组类型,并赋值给elementData。(调用Arrays.copyOf集合复制方法)
总结:
可以看到ArrayList的内部存储结构就是一个Object类型的数组,因此它可以存放任意类型的元素。在构造ArrayList的时候,如果传入初始大小那么它将新建一个指定容量的Object数组,如果不设置初始大小那么它将不会分配内存空间而是使用空的对象数组,在实际要放入元素时再进行内存分配。另外注意:初始化的参数大小和集合的size值是不一样的值,一个是数组的默认可存大小(或者说是数组长度),一个是集合的实际多少个。
4、核心函数
在介绍add之前,属性数组复制的两个方法:
方法一、 System.arraycopy:
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
参数分别为:src:源数组; srcPos:源数组要复制的起始位置;
dest:目的数组; destPos:目的数组放置的起始位置;
length:复制的长度。
方法二、Arrays.copyOf:
Arrays.copyOf(original,newLength);
其底层其实也调用的是System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
4.1、add函数
public boolean add(E e) { // 添加元素
ensureCapacityInternal(size + 1); // 确认容量大小
elementData[size++] = e; //将元素添加到末尾
return true;
}
在add函数我们发现还有其他的函数ensureCapacityInternal,此函数可以理解为确保elementData数组有合适的大小。ensureCapacityInternal的具体函数如下:
//确认容量大小
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 判断元素数组是否为空数组(构造时未赋初始容量,第一次add时才为true)
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 取较大值
}
ensureExplicitCapacity(minCapacity);
}
在ensureCapacityInternal函数中我们又发现了ensureExplicitCapacity函数,这个函数也是为了确保elemenData数组有合适的大小。ensureExplicitCapacity的具体函数如下:
//确认是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
// 结构性修改加1
modCount++;
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
在ensureExplicitCapacity函数我们又发现了grow函数,grow函数才会对数组进行扩容,ensureCapacityInternal、ensureExplicitCapacity都只是过程,最后完成实际扩容操作还是得看grow函数,grow函数的具体函数如下:
private void grow(int minCapacity) {
int oldCapacity = elementData.length; // 旧容量
int newCapacity = oldCapacity + (oldCapacity >> 1); //新数组的容量, 在原来的基础上增加一半
if (newCapacity - minCapacity < 0) // 新容量小于参数指定容量,修改新容量
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0) // //检验新的容量是否超过最大数组容量,超过则赋最大容量值给它
newCapacity = hugeCapacity(minCapacity); // 指定新容量
// 拷贝扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
说明:每次添加一个元素到集合中都会先检查容量是否足够,否则就进行扩容。正常情况下会扩容1.5倍,特殊情况下(新扩展数组大小已经达到了最大值)则只取最大值。
总结:
每次添加元素前会调用ensureCapacityInternal这个方法进行集合容量检查。在这个方法内部会检查当前集合的内部数组是否还是个空数组,如果是就新建默认大小为10的Object数组。如果不是则证明当前集合已经被初始化过,那么就调用ensureExplicitCapacity方法检查当前数组的容量是否满足这个最小所需容量,不满足的话就调用grow方法进行扩容。在grow方法内部可以看到,每次扩容都是增加原来数组长度的一半,扩容实际上是新建一个容量更大的数组,将原先数组的元素全部复制到新的数组上,然后再抛弃原先的数组转而使用新的数组。
add函数还有一种构造方法:
public void add(int index, E element) {
//插入位置范围检查
rangeCheckForAdd(index);
//检查是否需要扩容
ensureCapacityInternal(size + 1);
//挪动插入位置后面的元素
System.arraycopy(elementData, index, elementData, index + 1, size - index);
//在要插入的位置赋上新值
elementData[index] = element;
size++;
}
4.2、其他方法
//删
public E remove(int index) {
//index不能大于size
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0) {
//将index后面的元素向前挪动一位
System.arraycopy(elementData, index+1, elementData, index, numMoved);
}
//置空引用
elementData[--size] = null;
return oldValue;
}
//改
public E set(int index, E element) {
//index不能大于size
rangeCheck(index);
E oldValue = elementData(index);
//替换成新元素
elementData[index] = element;
return oldValue;
}
//查
public E get(int index) {
//index不能大于size
rangeCheck(index);
//返回指定位置元素
return elementData(index);
}
总结:具体增删改查要注意的地方
增(添加):仅是将这个元素添加到末尾。操作快速。
增(插入):由于需要移动插入位置后面的元素,并且涉及数组的复制,所以操作较慢。
删:由于需要将删除位置后面的元素向前挪动,也会设计数组复制,所以操作较慢。
改:直接对指定位置元素进行修改,不涉及元素挪动和数组复制,操作快速。
查:直接返回指定下标的数组元素,操作快速。
5、最后总结
5.1、ArrayList底层实现是基于数组的,因此对指定下标的查找和修改比较快,但是删除和插入操作比较慢;
5.2、构造ArrayList时尽量指定容量,减少扩容时带来的数组复制操作;
5.3、每次添加元素之前会检查是否需要扩容,每次扩容都是增加原有容量的一半;
5.4、每次对下标的操作都会进行安全性检查,如果出现数组越界就立即抛出异常;
5.5、ArrayList的所有方法都没有进行同步,因此它不是线程安全的。
转载博客:
jdk1.7版本ArrayList分析