前言
准备面试,从源码上看一下ArrayList的扩容机制
扩容机制
底层结构
就是一个Object数组,那么意味着对其初始化一个固定长度后,就不能改变了,要想改变,只能复制到新的数组中
初始化
三种方式,只需要了解前两种
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
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);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
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;
}
}
第一种
- 传入一个cap,并且为其new一个相应大小的Object数组
- cap为零则和第二种一样
第二种
- 不传参数
- this.elementData={}
- 空数组意味着没有空间添加元素,如果要添加元素则必须扩容
添加元素(add方法)
add方法同样有两种,主要看直接插入数组末尾的
public boolean add(E e) {
// 只需要保证数组长度为size+1,这样就能添加一个了
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 判断一下elementData是否为空,是的话就返回默认容量,DEFAULT_CAPACITY=10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
// minCapacity表示我至少需要多少容量,elementData.length表示我目前有多少容量
// 如果目前容量能满足我的需求,则无需扩容,否则扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 增加当前容量的一半作为新的容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 拷贝到新的数组中
elementData = Arrays.copyOf(elementData, newCapacity);
}
- 源码很简单,只需要思考几个问题
- 无参初始化的数组容量是多少?
- 初始化时为空数组
- 添加元素时会进行扩容,默认最小容量为10
- 什么时候会扩容?
- 初始化后不会扩容
- 添加元素时,如果需要的容量大于现有数组容量则扩容
- 怎么扩容?
- 计算新容量,为原来容量加上其一半
- 如果新容量大于需求容量,则扩容为新容量,否则直接以需求容量进行扩容(这意味着下次添加第二个元素后又会触发扩容)
- 注意事项
- 由上述扩容方式可知,容量较小时,若需要不断添加元素,会频繁触发扩容,因此对于可预知的元素长度,最好预设Cap进行初始化
至此,扩容原理梳理结束
例子
举个例子吧,在例子中,使用反射来获取elementData的容量
public static void arraylistCheck() throws NoSuchFieldException, IllegalAccessException {
System.out.println("case 1: ");
ArrayList<String> arr = new ArrayList<>();
System.out.println("初始化后:"+getCap(arr));
arr.add("11");
System.out.println("add 1次后:"+getCap(arr));
for (int i = 0; i < 13; i++) {
arr.add("11");
}
System.out.println("add 13次后:"+getCap(arr));
System.out.println("-----------------------------");
System.out.println("case 2: ");
ArrayList<String> arr2 = new ArrayList<>(12);
System.out.println("初始化后:"+getCap(arr2));
for (int i = 0; i < 13; i++) {
arr2.add("11");
}
System.out.println("add了13次后:"+getCap(arr2));
}
case 1:
初始化后:0
add 1次后:10
add 13次后:15
-----------------------------
case 2:
初始化后:12
add了13次后:18
说明
- case 1:
- 无参构造,初始化为空数组
- 添加一个元素后,触发扩容,默认为10
- 添加11个元素后,超过10,触发扩容,新的容量=10+10/2=15
- case 2:
- 有参构造,直接初始化一个长度为12的数组
- 添加第13个元素时,触发扩容,新的容量=12+12/2=18
再比较一下初始化容量与不初始化容量在性能的差异
public static void check() {
new Thread(() -> {
long start1 = System.nanoTime();
noPram();
long end1 = System.nanoTime();
System.out.println("noPram: "+(end1-start1));
}, "1").start();
new Thread(() -> {
long start1 = System.nanoTime();
hasPram();
long end1 = System.nanoTime();
System.out.println("hasPram: "+(end1-start1));
}, "1").start();
}
public static void noPram() {
ArrayList<String> strings = new ArrayList<>();
for (int i = 0; i < 100000000; i++) {
strings.add("111");
}
}
public static void hasPram() {
ArrayList<String> strings = new ArrayList<>(100000000);
for (int i = 0; i < 100000000; i++) {
strings.add("111");
}
}
hasPram: 679629900
noPram: 1600227000
性能差异在大数下还是比较明显的
流程图
画了个简单的流程图,带颜色部分为重点