Java集合框架是十分重要且具有学习意义的,今天对它的List接口下的ArrayList实现类的扩容机制进行深入学习,这篇文章也就应运而生。
目录
开始
ArrayList底层其实就是一个简单的用于存储Object类型的数组,如果不加以设置,那么初始化长度是0
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
其中 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是长度为0的空数组。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
那么ArrayList是什么时候初始化容量的呢?由ArrayList的源码文档可知,当添加第一个元素时,任何带有 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList 都将扩展到 DEFAULT_CAPACITY。
而这个 DEFAULT_CAPACITY 是什么呢?下面由一个小 demo ,带着我们一起去了解一下。
public class test01 {
public static void main(String[] args) {
outOfIndexWithAddOne();
//
}
private static void outOfIndexWithAddOne() {
ArrayList<String> list = new ArrayList<>();
list.add("你好");
String[] strs = {"a","a","a","a","a","a","a","a","a"};
list.addAll(Arrays.asList(strs));
// -----------------------------------
list.add("你好啊"); //TODO expanding the previous capacity.
System.out.println(list);
}
}
当调用 add() 方法向 ArrayList 中添加进第一个元素,数组的扩容机制就开始工作了。
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
//
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
//
private Object[] grow() {
return grow(size + 1);
}
grow() 方法就是扩容机制的重点
private Object[] grow(int minCapacity) {
/*
添加第一个元素时,oldCapacity 的值是0,if条件不成立
执行 new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
此时 minCapacity 的值为 1
*/
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
private static final int DEFAULT_CAPACITY = 10;
因此,往 ArrayList 容器中添加第一个元素时,容器容量被初始化为10。
如果接着往容器中存入元素使之超出初始容量10,容器会执行扩容机制,也就是if条件走的分支。
ArraysSupport.newLength 方法
我们先简单地介绍一下工具类 ArraysSupport 提供的 newLength 方法
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {}
这样一个公共的静态方法需要三个参数值,分别是:
- oldLength – current length of the array (must be nonnegative) :当前容器容量,必须是非负值
- minGrowth – minimum required growth amount (must be positive) :最小扩容量,同样必须是正数
- prefGrowth – preferred growth amount :首选增值(扩容量)
接下来一起看一下它的具体工作流程
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
}
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
// preconditions not checked because of inlining
// assert oldLength >= 0
// assert minGrowth > 0
int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
return prefLength;
} else {
// put code cold in a separate method
return hugeLength(oldLength, minGrowth);
}
}
扩容后的容量就是在老容量基础上加上合适的扩容长度。需要注意的是
int prefLength = oldLength + Math.max(minGrowth, prefGrowth);
可能会造成溢出。此时需要走 hugeLength() 方法
private static int hugeLength(int oldLength, int minGrowth) {
int minLength = oldLength + minGrowth;
if (minLength < 0) { // overflow
throw new OutOfMemoryError(
"Required array length " + oldLength + " + " + minGrowth + " is too large");
} else if (minLength <= SOFT_MAX_ARRAY_LENGTH) {
return SOFT_MAX_ARRAY_LENGTH;
} else {
return minLength;
}
}
这个方法里面,如果扩容后确实超过了 int 整形所能存储的最大值,就会抛出 OutOfMemoryError 异常。否则,就与 SOFT_MAX_ARRAY_LENGTH 的值进行比较,从而返回最合适的结果进行扩容。
常量值 SOFT_MAX_ARRAY_LENGTH 是什么
这里我们再来说说这个SOFT_MAX_ARRAY_LENGTH
它是数组增长计算施加的软最大数组长度。某些 JVM(如 HotSpot)具有实现限制,如果请求在 Integer.MAX_VALUE 附近分配某个长度的数组,即使有足够的堆可用,也会导致抛出 OutOfMemoryError(“请求的数组大小超过 VM 限制”)。实际限制可能取决于某些特定于 JVM 实现的特征,例如对象标头大小。保守地选择软最大值,以便小于可能遇到的任何实现限制。
public static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;
以上就是当 ArrayList 的初始化容量和扩容机制。但是如果在 ArrayList 容量无法支持大批元素的一次性加入,扩容机制又该如何实现?下面我们来研究一下
一次性添加多个元素超出 ArrayList 原容量
当调用 addAll() 方法一次性批量地填充元素到 array 中,此时 array 容量不足以支持加入,就需要另外一种扩容机制,而不是在原来的基础上慢慢扩充 array 的容量。
先看 demo
public class test01 {
public static void main(String[] args) {
outOfIndexWithAddAll();
//
}
private static void outOfIndexWithAddAll() {
ArrayList<String> list = new ArrayList<>();
list.add("你好");
String[] strs = {"a","a","a","a","a","a","a","a","a"};
list.addAll(Arrays.asList(strs));
// -----------------------------------
String[] strs2 = {"b","b","b","b","b","b","b","b","b"}; //TODO expanding the previous capacity.
list.addAll(Arrays.asList(strs2));
System.out.println(list);
}
}
扩容机制在 addAll() 中执行
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
modCount++;
int numNew = a.length;
if (numNew == 0)
return false;
Object[] elementData;
final int s;
if (numNew > (elementData = this.elementData).length - (s = size))
elementData = grow(s + numNew);
System.arraycopy(a, 0, elementData, s, numNew);
size = s + numNew;
return true;
}
如果加入的元素数量超出了原容器的容量,那么就调用 grow() 方法执行扩容,此时扩容的话是根据加入元素的数量大小进行扩容的:
- 如果原容器还没被填充满,就只扩容需要的容量;
- 如果原容器已经满了,就扩容添加的元素的数量
如图
最后再对数据进行拷贝到扩容后的容器中。
System.arraycopy 方法
这里再补充讲一下 System.arraycopy 方法
这一个方法是一个被 native 修饰的方法,用途就是数组或者集合间的拷贝。
@IntrinsicCandidate
public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length);
它需要 5 个参数值:
- Object src:源数组
- int srcPos:源数组中的起始位置
- Object dest:目标数组
- int destPos:目标数组中的起始位置
- int length:要复制的数组元素长度
注意:这里的起始位置指的都是拷贝的起始位置 。int length 指的是要从原数组中拷贝到目标数组的元素个数
测试