参考资料:JDK 8 中 ArrayList 的自动扩容 要详细了解序列化,可以参考该博客:Java 中的 transient 关键字
先说结论
源码解读
我们知道数组本身是无法直接扩容的,要想存储更多的数据需要新建一个数组,再把之前数组的所有内容填充进去。而 ArrayList 的自动扩容机制,使得其在处理变长度数组的时候非常方便。
字段
在了解扩容机制之前,先对 ArrayList 中的各个字段进行解读:
// 用于支持 Serializable 机制,与扩容无关
private static final long serialVersionUID = 8683452581122892189L;
// 默认的扩容量的长度为10
private static final int DEFAULT_CAPACITY = 10;
// 在类加载时就定义的两个空元素数组
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
// ArrayList 中元素存储的底层实现,本质是一个Object数组
transient Object[] elementData;
// 存储当前 ArrayList 中元素的数量
private int size;
可以看到,本质上 ArrayList 底层用于存储元素的字段是一个 Object 数组,这个数组使用了 transient 关键字修饰,直译是“暂时的,临时的”,在 Java 中的含义是不可序列化的,即在程序运行过程中该变量只会存在于调用者的内存中,而不会写入磁盘。
构造器
接下来查看 ArrayList 是怎么初始化的, 在 ArrayList 类中给出了如下三个构造器:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else {
if (initialCapacity != 0) {
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
this.elementData = EMPTY_ELEMENTDATA;
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((this.size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
this.elementData = a;
} else {
this.elementData = Arrays.copyOf(a, this.size, Object[].class);
}
} else {
this.elementData = EMPTY_ELEMENTDATA;
}
}
可以看到,第一个和第三个构造器调用的时候,会按照所传参数初始化 elementData 数组,数组大小与所传参数有关。当传入的参数小于等于0(调用第一个构造器)或传入了一个空的 Collection 对象(调用第三个构造器)时,elementData 数组会被初始化为 EMPTY_ELEMENTDATA 数组,它是一个空的 Object 数组对象。而在调用默认构造器的时候,elementData 数组会被初始化为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组,它也是一个空的 Object 数组对象。
add()
当我们向 ArrayList 对象添加元素的时候,可能会发生 elementData 数组原本的长度不足,这时候就应该触发自动扩容机制,所以接下来查看 add() 方法是如何处理的。
private void add(E e, Object[] elementData, int s) {
// 触发自动扩容机制
if (s == elementData.length) {
elementData = this.grow();
}
elementData[s] = e;
this.size = s + 1;
}
public boolean add(E e) {
++this.modCount;
this.add(e, this.elementData, this.size);
return true;
}
public void add(int index, E element) {
this.rangeCheckForAdd(index);
++this.modCount;
int s;
Object[] elementData;
// 触发自动扩容机制
if ((s = this.size) == (elementData = this.elementData).length) {
elementData = this.grow();
}
System.arraycopy(elementData, index, elementData, index + 1, s - index);
elementData[index] = element;
this.size = s + 1;
}
可以看到,如果直接插入某一元素,会在内部调用私有的 add() 方法,该方法会将 size 字段的值和 elementData 数组的大小进行比较,当二者相等,也即 elementData 数组存满了的时候,就会调用 grow() 方法。同样地,如果在某一个索引处添加某个元素,也会触发这一机制。
grow()
接下来考察 grow() 方法是如何实现的,源码如下所示:
private Object[] grow(int minCapacity) {
int oldCapacity = this.elementData.length;
if (oldCapacity <= 0 && this.elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return this.elementData = new Object[Math.max(10, minCapacity)];
} else {
int newCapacity = ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, oldCapacity >> 1);
return this.elementData = Arrays.copyOf(this.elementData, newCapacity);
}
}
private Object[] grow() {
return this.grow(this.size + 1);
}
当外部调用无参的 grow() 方法的时候,该方法会往有参的 grow() 方法中传入一个参数,该参数为目前 ArrayList 中实际存储的元素数量 +1,表示本次扩容所需的最小容量。因此可以想见,对于 addAll() 这样的一次插入多个元素的方法而言,其会直接调用有参的 grow() 方法,实际也正是如此。
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
++this.modCount;
int numNew = a.length;
if (numNew == 0) {
return false;
} else {
Object[] elementData;
int s;
// 触发扩容机制
if (numNew > (elementData = this.elementData).length - (s = this.size)) {
elementData = this.grow(s + numNew);
}
System.arraycopy(a, 0, elementData, s, numNew);
this.size = s + numNew;
return true;
}
}
public boolean addAll(int index, Collection<? extends E> c) {
this.rangeCheckForAdd(index);
Object[] a = c.toArray();
++this.modCount;
int numNew = a.length;
if (numNew == 0) {
return false;
} else {
Object[] elementData;
int s;
// 触发扩容机制
if (numNew > (elementData = this.elementData).length - (s = this.size)) {
elementData = this.grow(s + numNew);
}
int numMoved = s - index;
if (numMoved > 0) {
System.arraycopy(elementData, index, elementData, index + numNew, numMoved);
}
System.arraycopy(a, 0, elementData, index, numNew);
this.size = s + numNew;
return true;
}
}
在有参的 grow() 方法中,先读取扩容之前的 elementData 数组的长度,如果该数组之前长度就为0并且就是字段中对应的 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 数组(即调用的是无参构造器),就将 elementData 数组扩容到需求的最小长度,如果这个最小长度比10小,那就会直接扩容到10;而如果该数组之前长度不为0,或者初始化的时候调用的不是无参构造器,那么就会调用 ArraySupports 类中的 newLength 方法来获得需要增长的长度,并返回扩容后新的 elementData 数组。newLength 方法是这样定义的:
// 调用语句
ArraysSupport.newLength(oldCapacity, minCapacity - oldCapacity, oldCapacity >> 1);
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
return newLength - 2147483639 <= 0 ? newLength : hugeLength(oldLength, minGrowth);
}
该方法有三个参数,第一个参数代表之前容量,第二个参数代表最少应该增加的容量,第三个参数代表优先考虑增加的容量,最终增加的容量大小会根据后两者中的较大值来确定,最后还会添加一个判断来保证返回值不会溢出。
而 grow() 方法中传入的第三个参数为过去的值按位右移1位,可以理解为整型计算中的除以2。至此我们可以得到整个自动扩容的流程,将其整理为流程图为: