ArrayList底层数据结构就是数组,用数组来存储元素。既然是用到数组,就涉及到扩容的问题。
size
变量是实际存储元素的个数,项目代码中通常用ArrayList.size()
方法来获取元素的个数。数组容量
,是指存储元素的数组的长度:elementData.length
。
3个构造方法
java
/**
* 默认初始容量大小
*/
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* 默认构造函数,使用初始容量10构造一个空列表(无参数构造)
* 如果不往里面add元素,实际列表的容量是0
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
/**
* 带初始容量参数的构造函数。(用户自己指定容量)
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {//初始容量大于0
//创建initialCapacity大小的数组
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {//初始容量等于0
//创建空数组
this.elementData = EMPTY_ELEMENTDATA;
} else {//初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}
/**
* 构造包含指定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;
}
}
以无参数构造方法创建ArrayList
时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10 (如果是调用有参构造方法创建ArrayList,debug之后看了初始化赋值的也是一个空数组,数组容量并不是传入的大小。?)
扩容入口:add()方法
java
public boolean add(E e) {
// 加元素之前,先调用ensureCapacityInternal方法,
ensureCapacityInternal(size + 1); // Increments modCount!!
// 这里看到ArrayList添加元素的实质就相当于为数组赋值
elementData[size++] = e;
return true;
}
判断是否扩容:ensureCapacityInternal()
方法
上述add方法调用的ensureCapacityInternal()
方法如下
java
// 直接调用calculateCapacity和ensureExplicitCapacity方法
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
// 根据给定的最小容量和当前数组元素来计算所需容量。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 满足该条件,即为初始数组为空的情况。此时所需容量取默认容量和最小容量中的最大值
// 初始情况下,minCapacity总是1,所以首次计算的容量是10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 判断当前数组容量是否足以存储上述计算出来的minCapacity个元素。
// 如果当前数组容量不够,则需要调用grow方法,进行扩容。
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果当前数组大小,小于minCapacity,则需要进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
判断是否扩容流程图
总结
- 若调用无参的构造方法进行初始化,则在添加第1个元素的时候需要进行一次扩容,进入
grow
方法 - 当add第2个元素时,
minCapacity
为2(size+1),此时elementData.length(数组容量)
在添加第1个元素后扩容成10了,所以此时不会进入grow方法。同理添加第3、4、...10个元素时,都不会触发扩容。 - 添加第11个元素的时候,
minCapacity
为11,比elementData.length(为10)
要大,所以需要扩容
扩容的步骤:grow()方法
java
// 核心思想是:创建一个更大的新数组,将老数组的元素复制到新数组中。
/**
* 要分配的最大数组大小
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// overflow-conscious code
// oldCapacity为旧容量,newCapacity为新容量。
int oldCapacity = elementData.length;
// 将oldCapacity右移一位,相当于oldCapacity/2。
// 计算结果为:newCapacity为oldCapacity的1.5倍。
// ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)!
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 若计算的新容量仍小于最小需要的容量,则将新容量赋值为最小需要的容量。
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于最大数组容量,则调用hugeCapacity方法比较 minCapacity 和 MAX_ARRAY_SIZE,规则如下:
// 如果minCapacity大于最大容量,则新容量为Integer.MAX_VALUE;
// 否则,新容量大小为 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);
}
-
当
add
第 1 个元素时,oldCapacity
为 0,经比较后第一个 if 判断成立,newCapacity = minCapacity
(为 10)。但是第二个 if 判断不会成立,即newCapacity
不比MAX_ARRAY_SIZE
大,则不会进入hugeCapacity
方法。数组容量为 10,add
方法中 return true,size 增为 1。 -
当
add
第 11 个元素进入grow
方法时,newCapacity
为 15,比minCapacity
(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 hugeC
apacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。 -
以此类推······
这里补充一点比较重要,但是容易被忽视掉的知识点:
- Java 中的
length
属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性。 - Java 中的
length()
方法是针对字符串说的,如果想看这个字符串的长度则用到length()
这个方法。 - Java 中的
size()
方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看。