ArrayList扩容机制
先认识一下ArrayList的数据结构吧,要分析ArrayList的扩容机制,我们需要提前知道的属性有:
ArrayList的成员变量/常量
// 默认的初始化容量
private static final int DEFAULT_CAPACITY = 10;
// new出ArrayList 对象时,指定了初始容量为0
private static final Object[] EMPTY_ELEMENTDATA = {};
// 直接使用无参构造函数时的数据
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 直接使用无参构造函数时将该对象的
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 真正存放数据的数组对象
transient Object[] elementData;
// 集合的大小
private int size;
ArrayList的构造方法
/**
* 用户自己指定初始容量参数的构造函数。
*/
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);
}
}
/**
* 默认构造函数,使用初始容量10构造一个空集合
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
*构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
*如果指定的集合为null,throws NullPointerException。
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
可能有的朋友已经注意到,在三个构造函数中,会为elementData指定一个空数组,在无参构造函数中这个空数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA,而在有参构造函数中这个空数组却是EMPTY_ELEMENTDATA,那么它两有什么区别呢?我会在文章最后谈谈我的理解,现在我们还是回头看ArrayList是如何完成一次扩容的。
ArrayList扩容分析
我们知道,如果ArrayList要进行扩容,肯定要满足一个条件,那就是ArrayList的容量 “ 不够用 ” 了,所以我们自然想到了ArrayList中常见的添加元素的四种方法:
1. add(E e)
2. add(int index, E e)
3. addAll(Collection<? extends E> c)
4. addAll(int index, Collection<? extends E> c)
对于ArrayList的扩容,其核心思想就是把原数组用一个更大容量的新数组来代替,然后将原数组的数据拷贝到新的数组中来,所以这里我就以add方法举例
/**
* 将指定的元素添加到list的末尾
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
显然方法体中的第二行代码是将元素添加的元素加在list末尾,那么扩容应该就是在ensureCapacityInternal方法中了
/**
* minCapacity:最小容量
*/
private void ensureCapacityInternal(int minCapacity) {
// 如果是通过无参构造函数创建的ArrayList对象
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 获取默认的容量和传入参数的较大值
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // modCount记录ArrayList add 和remove 的次数
// overflow-conscious code
//如果最小容量大于了当前数组的最大长度,显然需要扩容了
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
我们来详细分析一下上面的两个方法,在前面我们已经说到过,ArrayList有三种构造方法,我们可以把他们分为有参和无参两类,在使用无参构造方法时,elementData 会被赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA;当使用有参构造方法时,我们先假设初始容量也为0,那么elementData会被初始化为EMPTY_ELEMENTDATA,在执行add方法的时候,add方法调用ensureCapacityInternal方法,ensureCapacityInternal方法会判断elementData是不是等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,如果等于则让最小容量等于默认的容量DEFAULT_CAPACITY和传入参数两者中的较大值。这里我们可以思考,在第一次,第二次一直到第10次,最小容量都会取为DEFAULT_CAPACITY,在判断之后会调用ensureExplicitCapacity,在ensureExplicitCapacity方法中,grow就是最后扩容的方法了。但是在扩容之前,会判断一下现在最小容量是不是超过了elementData数组的长度,只有超过时,才会去执行扩容操作。下面我们接着看看扩容的方法
private void grow(int minCapacity) {
// overflow-conscious code
// 扩容前容量
int oldCapacity = elementData.length;
// 扩容后容量,这里使用了位运算,每次尝试扩容约1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果发现newCapacity仍然小于最小容量,则直接使用最小容量作为扩容后的容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
/**
* 如果newCapacity 很大,甚至已经大于了大于了MAX_ARRAY_SIZE,则试着用最小容量去判断一下。
* 在hugeCapacity方法中,如果最小容量minCapacity 也大于MAX_ARRAY_SIZE ,则一步到位,使用Integer的最大值作为扩容后的容量
* 如果最小容量minCapacity小于MAX_ARRAY_SIZE 则使用MAX_ARRAY_SIZE作为扩容后的容量,MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
*/
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;
}
由以上我们可以得出一个结论:
- 如果使用无参构造方法创建了一个ArrayList对象,那么在第一次执行add操作的时候,ArrayList会进行扩容,并且容量直接扩容为10。而使用有参构造函数(初始容量指定为0)进行扩容时,第一次add操作会直接触发扩容操作,容量会指定为1。
- ArrayList的每一次扩容都会执行位运算计算新容量,位运算的结果是新容量约为旧容量的1.5倍,然后在新容量、最小容量、MAX_ARRAY_SIZE 与
Integer.MAX_VALUE之间取一个合适的值作为扩容后的容量 - 容量大于Integer.MAX_VALUE会抛出OutOfMemoryError
以上差不多就是ArrayList的整个扩容过程了,其实在分析其扩容的过程中,不免产生下面的疑问,为什么要定义两个空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA?
要回答这个问题还得看看JDK1.7中ArrayList的构造方法
public ArrayList(int initialCapacity) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
}
public ArrayList() {
super();
this.elementData = EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
对比JDK1.8的代码可以发现,在JDK1.8中完全用DEFAULTCAPACITY_EMPTY_ELEMENTDATA取代了EMPTY_ELEMENTDATA,那么EMPTY_ELEMENTDATA就没用了吗?其实也不是在JDK1.7中,如果指定初始容量是0的话,每次都会会new 一个Object数组,如果内存中存在大量的这样的数组显然对性能有一些损耗,所以从性能的角度来看,就直接将EMPTY_ELEMENTDATA的引用赋值给ArrayList空示例。其实进一步想,一个空数组完全能够实现上面的功能,并且在JDK1.8的源码中还做了很多elementData 是不是等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA的判断,源码的作者为什么要这么做我目前也没有想明白,希望了解这一点的读者能在评论区留言赐教一下。