本篇分析 ArrayList 的源码,在分析以前先跟你们谈一谈数组。数组多是咱们最先接触到的数据结构之一,它是在内存中划分出一块连续的地址空间用来进行元素的存储,因为它直接操做内存,因此数组的性能要比集合类更好一些,这是使用数组的一大优点。可是咱们知道数组存在致命的缺陷,就是在初始化时必须指定数组大小,而且在后续操做中不能再更改数组的大小。在实际状况中咱们遇到更多的是一开始并不知道要存放多少元素,而是但愿容器可以自动的扩展它自身的容量以便可以存放更多的元素。ArrayList 就可以很好的知足这样的需求,它可以自动扩展大小以适应存储元素的不断增长。它的底层是基于数组实现的,所以它具备数组的一些特色,例如查找修改快而插入删除慢。本篇咱们将深刻源码看看它是怎样对数组进行封装的。首先看看它的成员变量和三个主要的构造器。java
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8683452581122892189L;
/** * 默认初始化的容量 */
private static final int DEFAULT_CAPACITY = 10;
/** * 初始化一个空实例的共享空数组实例。 */
private static final Object[] EMPTY_ELEMENTDATA = {};
/** *用于默认大小的空实例的共享空数组实例。咱们将此与 empty _ elemtdata 区分开来, 以了解 *添加分离第一元素时充气的程度 */
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/** * 元素对象数据 */
transient Object[] elementData; // non-private to simplify nested class access
/** * 当前 集合 的大小 * * @serial */
private int size;
/** * 建立 ArrayList 的时候直接给元素对象开辟一个指定的容量大小 * 不然 直接初始化一个空的元素对象 * * @param * @throws * */
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/** * 若是直接 new 一个空的构造函数的 ArrayList 内部直接建立一个默认空的对象数组 */
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/** * * @param c 外部直接传入一个集合 * @throws NullPointerException if the specified collection is null */
public ArrayList(Collection<? extends E> c) {
//将外部传入进来的集合数据转化为数组赋值给 元素对象数组
elementData = c.toArray();
//更新集合大小
if ((size = elementData.length) != 0) {
// 判断引用的数据类型,并将引用转换成 Object 数组引用
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// 若是传入进来的是一个空的集合,那么直接建立一个空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
复制代码
能够看到 ArrayList 的内部存储结构就是一个 Object 类型的数组,所以它能够存听任意类型的元素。在构造ArrayList 的时候,若是传入初始大小那么它将新建一个指定容量的 Object 数组,若是不设置初始大小那么它将不会分配内存空间而是使用空的对象数组,在实际要放入元素时再进行内存分配。下面再看看它的增删改查方法。数组
/** * @param 将 e 元素添加到当前 elementData 中 * @return 返回是否添加成功 */
public boolean add(E e) {
// 每次添加数据的时候都要检查当前数组容量是否须要扩容
ensureCapacityInternal(size + 1);
// 直接赋值
elementData[size++] = e;
return true;
}
/** * * * @param index 根据外部传入的 index 来进行存储数据 * @param element element to be inserted * @throws IndexOutOfBoundsException {@inheritDoc} */
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//检查是否须要扩容
ensureCapacityInternal(size + 1);
//将须要插入的 index 位置日后移动一位
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
//在 数组的 index 位置直接存入 元素
elementData[index] = element;
size++;
}
复制代码
public E remove(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
//将index后面的元素向前挪动一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 置空引用
elementData[--size] = null;
//返回被删除的元素
return oldValue;
}
/** * * @param 删除外部传入的对象 * @return 是否删除成功 */
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;
}
/** * 删除所有 * be empty after this call returns. */
public void clear() {
modCount++;
// 直接把容量空间所有置为 NULL 交于 GC
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
复制代码
/**
* @return 返回旧元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E set(int index, E element) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
//取出要改变的旧元素
E oldValue = (E) elementData[index];
//将新元素更新
elementData[index] = element;
return oldValue;
}
复制代码
/**
* @param 根据索引直接从数据元素中获取
* @return 返回取出的数据
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
return (E) elementData[index];
}
复制代码
每次添加一个元素到集合中都会先检查容量是否足够,不然就进行扩容,扩容的细节下面会讲到。咱们先看具体增删改查要注意的地方。安全
增(添加):仅是将这个元素添加到末尾。操做快速。bash
增(插入):因为须要移动插入位置后面的元素,而且涉及数组的复制,因此操做较慢。数据结构
删:因为须要将删除位置后面的元素向前挪动,也会设计数组复制,因此操做较慢。dom
改:直接对指定位置元素进行修改,不涉及元素挪动和数组复制,操做快速。函数
查:直接返回指定下标的数组元素,操做快速。性能
经过源码看到,因为查找和修改直接定位到数组下标,不涉及元素挪动和数组复制因此较快,而插入删除因为要挪动元素,涉及到数组复制,操做较慢。而且每次添加操做还可能进行数组扩容,也会影响到性能。下面咱们看看ArrayList是怎样动态扩容的。ui
private void ensureCapacityInternal(int minCapacity) {
//若是当前元素数据为空的时候
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 和默认的扩容容量比较,取出最大值。
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 数组初始化完毕进行下一步
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 若是最小的扩容容量 比 当前的 元素数据还要大的时候进行扩容,下面是扩容的核心代码
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
*
*扩容核心代码
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//扩容当前的容量的 1.5 倍
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);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
复制代码
每次添加元素前会调用 ensureCapacityInternal 这个方法进行集合容量检查。在这个方法内部会检查当前集合的内部数组是否仍是个空数组,若是是就新建默认大小为 10 的 Object 数组。若是不是则证实当前集合已经被初始化过,那么就调用 ensureExplicitCapacity 方法检查当前数组的容量是否知足这个最小所需容量,不知足的话就调用 grow 方法进行扩容。在 grow 方法内部能够看到,每次扩容都是增长原来数组长度的一半,扩容其实是新建一个容量更大的数组,将原先数组的元素所有复制到新的数组上,而后再抛弃原先的数组转而使用新的数组。至此,咱们对 ArrayList 中比较经常使用的方法作了分析,其中有些值得注意的要点:this