一、前言
ArrayList 即 List 的动态数组实现,底层实现机制就是动态数组,且支持动态扩容。
特点是:
- 非线程安全;
- 底层实现是数组,即集合中的元素是内存连续的,可通过元素索引下标 index 实现 O(1) 时间内的随机访问;
- 集合中元素是有序可重复的,支持 NULL;
- 集合能根据需要自动扩容;
ArrayList 中元素是内存连续的,因此根据索引下标 index 随机访问或遍历性能较高;但涉及到元素随机插入删除(元素插入触发扩容时会造成所有元素移动,从中间删除会涉及到后续所有元素移动)较多的场景性能较差。
二、ArrayList 源码解读
2.1 构造方法初始化
默认无参构造方法
这是我们最常用的场景,调用无参构造函数实例化 ArrayList 对象后,内存实际指向的是一个空数组,此时集合 capacity(容量)为 0;
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
指定 initialCapacity(集合初始容量)参数构造方法
即此时会新开辟一块容量大小为 initialCapacity 的连续内存空间;
注意:如使用 ArrayList 时明确知道会需要较大容量,此时可通过在构造函数初始化时使用此方法指定初始容量,减少自动扩容移动元素次数对性能的影响;
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
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);
}
}
指定 c(集合)参数构造方法
即将一个集合 c 作为参数构造 ArrayList 实例,此时会开辟一块连续内存用于存储集合中元素,ArrayList 实例 capacity(容量)大小为集合 c 长度;
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
2.2 添加元素 add 方法
注意,当使用无参构造函数实例化 ArrayList 对象时,默认是个空数组容量为0,在首次调用 add 方法添加元素时才会自动扩容为 10(即 对应 DEFAULT_CAPACITY参数值 )。
这一点在面试时候需特别注意,面试官问 ArrayList 初始默认容量大小,如果直接回答说 10 容易给面试官留下知识点混淆不清的印象。
此时标准答案可以说:ArrayList 使用无参构造函数实例化时初始容量是 0,在首次调用 add 方法添加元素时会进行第一次扩容,且大小默认为 10。
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
/*
*
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return <tt>true</tt> (as specified by {@link Collection#add})
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
2.3 ArrayList 扩容
什么时候触发扩容
当 minCapacity - elementData.length > 0 时,会调用 grow 方法触发扩容;
其中,
minCapacity 即集合最小容量(当前集合长度 size + 要添加元素个数),添加单个元素即 minCapacity = size + 1;
elementData.length 即底层数组长度大小;
下面代码有2个关键点需要注意:
- 无参构造实例化 ArrayList 对象后首次添加一个元素时,此时 minCapacity = 0 + 1,elementData.length = 0,即会触发第一次扩容,此时扩容容量为 10,这个默认长度设值要结合 grow 方法实现;
- 触发扩容时机要注意;第一次扩容后 elementData.length = 10,此时要第一次满足 minCapacity - elementData.length > 0 时 minCapacity 值为 11,就是达到当前容量 10 后再添加元素时会触发再次扩容,即扩容触发时机是:当前元素数量达到数组长度后,会在下一次添加元素时触发扩容。
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
扩容方法 grow 实现
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
oldCapacity >> 1 右移一位,即最后一位舍弃,其他位向右移动一位,相当于 oldCapacity / 2(举例:10 / 2 = 5, 15 / 2 = 7);
因此当问到 ArrayList 扩容时候会扩容到多大呢?
标准答案可以说:集合扩容后新容量,应为集合数据当前容量再加上集合数据当前容量大小的一半大小(向下取整);
集合扩容后新容量 = 集合数据当前容量 + 集合数据当前容量 / 2;
第 n 次扩容 | 集合扩容后新容量 |
---|---|
一 | 10 |
二 | 10 + 10 / 2 = 15 |
三 | 15 + 15 / 2 = 22 |
四 | 22 + 22 / 2 = 33 |
五 | 33 + 33 / 2= 49 |
六 | 49 + 49 / 2= 73 |
你可能会突然想到,首次扩容时 elementData.length 为 0,套用上述公式计算出的扩容后新容量还是 0,这不就是一个反例吗?
是的,在首次扩容 elementData.length 为 0 时这样计算当然不对,因此代码后续还有个重新判断赋值;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;}
即首次扩容时,newCapacity 根据上述扩容公式计算出为 0,minCapacity 为设置的默认容量 10,此时判断成立即进行一次重新赋值,newCapacity = minCapacity = 10,这也是我们说首次扩容后容量大小为 10 的原因所在;
后续代码还有个判断 newCapacity - MAX_ARRAY_SIZE > 0,这是在容量达到 MAX_ARRAY_SIZE(MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8)时候的特殊处理,容量达到整型上限场景很少,我们大概看下实现即可无需过多关注此处省略;
/**
* The maximum size of array to allocate.
* Some VMs reserve some header words in an array.
* Attempts to allocate larger arrays may result in
* OutOfMemoryError: Requested array size exceeds VM limit
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* Increases the capacity to ensure that it can hold at least the
* number of elements specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
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;
}