ArrayList是如何实现动态扩容的
前言
我们都知道ArrayList是基于动态数组的实现,当需要的长度大于自身最大的容量的时候,能自己扩容。那么它是怎么扩容的呢?每次扩容多少呢?还有初始的长度是多少呢?
一、我们先通过了解一下ArrayList的构造方法
从图中我们可以看到ArrayList有三个构造方法。
一个无参构造方法,一个传入一个int型的构造方法,另一个传入指定的集合元素。第一个构造方法的描述是构造一个初始容量为10的空列表,那么当我们以一个无参构造方法创建一个ArrayList列表时,可以说此时的列表的最大容量为10吗?其实并不是这样的,让我们走进原码探索一下。
二、走进原码
以下面代码为例:
ArrayList<Integer> date = new ArrayList<>();
date.add(996);
找到ArrayList的无参构造方法:
等号左边的原码:
transient Object[] elementData;
等号右边的原码:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
结论:其实我们通过无参构造方法创建的集合是一个长度为0的数组。
data.add();
当要往数组里面添加数据时,是先会判断数组的长度是否已达到最大容量,如果发现存不了,就扩容。调用add方法如果不出现异常就一定是返回true。
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
调用add方法就一定是返回true,因为就算你数组已经放不下东西了,也会自动进行扩容,在不出异常的情况下(这种异常一般是关于越界问题)所以只要传进来对的数据类型的数据就不会出现添加不成功的情况。
//传入三个参数,分别是:要添加内容,存数据的数组和数组的当前存储(有效)的长度
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow(); //如果已到达最大容量,存不下了,则进行扩容算法,返回一个数组
elementData[s] = e;
size = s + 1;
}
那这个grow() 方法(扩容算法)到底是怎么样的呢?
原码如下:
private Object[] grow() {
return grow(size + 1); //因为现数组已经不能再存新数据了,所以容量至少要加一
}
转到一个含有一个int型参数的grow方法
private Object[] grow(int minCapacity) { //size+1 例如原长度为0则传入1
int oldCapacity = elementData.length; //旧数组的长度
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
//判断如果传进来的数组是不是长度为0的数组
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
//oldCapacity>>1 意思是指向右移一位,比如oldCapacity原来的值为6,二进制:110,向右移一位:11,转为10进制为3,所以相当于oldCapacity*0.5
//最终newCapacity的值是oldCapacity*1.5,具体来源,感兴趣的继续看原码,这里就不再说明了。
return elementData = Arrays.copyOf(elementData, newCapacity);
//将一个旧数组赋值到一个新长度的数组,并返回新数组
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
//如果传进来的是一个长度为0的数组,则返回在默认值和旧长度+1(minCapacity)中的最大值,而这个默认值,就是我们要寻找的10
}
}
在上面代码的最后一个return中,Ctrl+单击进DEFAULT_CAPACITY 原码为:
private static final int DEFAULT_CAPACITY = 10;
所以说初始化容量为10,,,,这个10就是在这里来的。
三、扩展
实际上在这一步,传入的数组不为长度为0的数组时获取的newCapacity不一定是旧数组长度的1.5倍。我们Ctrl+点击上图红色的内容
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
// assert oldLength >= 0
// assert minGrowth > 0
//oldlength:旧数组的长度 minGrowth:要求至少扩容的长度 prefGrowth:原数组长度的0.5倍
int newLength = Math.max(minGrowth, prefGrowth) + oldLength;
if (newLength - MAX_ARRAY_LENGTH <= 0) {
//MAX_ARRAY_LENGTH(默认值):Integer.MAX_VALUE(Integer类型最大值) - 8
//说明比最大值小,可以直接返回扩容的长度
return newLength;
}
//r如果比最小值,执行下面这一行
return hugeLength(oldLength, minGrowth);
}
Ctrl+点击 hugeLength:
private static int hugeLength(int oldLength, int minGrowth) {
int minLength = oldLength + minGrowth;
if (minLength < 0) { // overflow 内存溢出了
throw new OutOfMemoryError("Required array length too large");
}
if (minLength <= MAX_ARRAY_LENGTH) {
return MAX_ARRAY_LENGTH;
}
return Integer.MAX_VALUE;
//minLength 的值就是在Integer.MAX_VALUE和Integer.MAX_VALUE-8之间,就直接返回Integer.MAX_VALUE
}
所以说返回的不一定是原数组的1.5倍长度,也有可能因为越界抛出异常,也可能是返回长度为Integer.MAX_VALUE或Integer.MAX_VALUE-8
四、注意&总结
1、ArrayList:使用的是数组结构,对增加删除慢,查找块,所以说以后不能什么都用ArrayList来操作。
2、ArrayList<Integer> date = new ArrayList<>();
在这里确定列表的类型时,不能用int,传入的一定是包装类
3、如果使用无参构造方法,每当容量到达最大量就会自动扩容原来容量的1.5倍。如果扩容前的容量为0,则先扩容容量为10。
4、如果我们最终的需要的容量是1000, 而我们原先是用无参构造方法进行创建列表,当每次达到最大容量,就会频繁地进行数组的扩容操作,由于数组的扩容是将旧的数组的值复制到新的数组,而旧的数组就撇弃掉,从而浪费大量的内存。所以说如果初次存储时需要大量的容量,那么建议用传入int型的一参构造方法,确定初始容量,就算初始的容量不够,那么我们再扩容就是了。