关于ArrayList扩容机制的源码分析(基于jdk1.7版本)
在jdk1.7版本中,我们创建ArrayList的对象调用无参构造器时,在其构造器内部调用了重载的带参构造器,并且将参数赋值为10。
此时便创建了数组长度为10的Object[]的elementData。
接着我们再继续看add()方法
当我们第一次添加元素时size为0,那么minCapacity便是0 + 1 = 1。进入ensureCapacityInternal()方法,看其内部代码逻辑是怎么处理的
modCount变量暂且忽略(涉及到ArrayList快速失败机制,分析扩容机制忽略它无大碍)
minCapacity的值为1,elementData数组的长度为10(调用ArrayList无参构造器时初始化的长度)。很明显,无法进入grow()方法。说明添加第一个元素的时候无需扩容,初始化的数组长度是可以盛装添加的元素的。当我们要添加第11个元素的时候。add()方法中的size长度为10,那此时minCapacity的值为11,if()判断语句成立,进入grow()方法。我们继续进入grow()方法内部查看代码逻辑的处理方式。
代码片段中声明的oldCapacity变量为旧的容量,相当于我们将要进行扩容之前的集合容量(在这就相当于我们初始化的数组长度,因为这是我们在初始化过容量后的第一次扩容)。不难看出代码中将初始化的数组长度赋值给oldCapacity为10。然后就是将oldCapacity右移一位(相当于oldCapacity/2)并加上oldCapacity。可以计算出newCapacity为15。可见扩容的容量为原数组长度的一半。在一般情况下确是如此。拿到新的容量newCapacity,调用Arrays工具类下的copyOf进行复制数组的操作,将返回的新的数组长度与原有数组中的数据(也就是扩容1.5倍后的长度)赋值给elementData。
上面提到过,在一般情况下确是如此,如果我们调用带参构造器,自己设置参数为1,这时便不符合此规律了。那就接着分析,不符合此种规律的扩容是如何进行处理的。如果我们调用ArrayList的带参构造器,并且设置参数为1。当我们添加第二个元素的时候便会进入grow()方法进行扩容操作。那么此时oldCapacity的值为1。再进行右移1位操作(1 / 2,因为是int类型,所以oldCapacity的值为0)再加上本身得到的值便还是1,再将1赋值给newCapacity, 此时会进入grow()方法中的第一个if判断语句中(newCapacity的值为1,minCapacity为2),不难看出最后将minCapacity的值直接赋给了newCapacity。也就是说如果源码计算出来的新的容量还是小于我们需要扩容的容量的话,就直接将我们需要扩容的容量当作新的容量。这是第一种情况,还有一种情况,就是当我们要添加的元素非常多,需要的新的容量已经大于MAX_ARRAY_SIZE的时候。继续分析。先看下MAX_ARRAY_SIZE这个常量是如何定义的
Integer类型的最大值减去8。我们进入hugeCapacity()方法分析。
当minCapacity小于0便会报内存溢出错误。什么时候minCapacity会小于0呢?很疑惑,因为我们都是再进行扩容操作,肯定是不断变大的。当minCapacity大于Integer.MAX_VALUE的时候就会报内存溢出了。如果没有大于Integer的最大值但是大于MAX_ARRAY_SIZE,便返回Integer.MAX_VALUE。反之,返回MAX_ARRAY_SIZE。
总结:在jdk1.7中创建ArrayList对象时,不管是调用无参构造器还是带参构造器,在底层都会创建一个固定长度的数组。如果是调用的无参构造器便会创建一个长度为10的Object[]数组elementData。如果是调用的带参构造器,便会根据我们传入的参数创建一个固定长度的数组。在这点上与jdk1.8版本的扩容会略有不同,不过后续的扩容操作是没差的。
结语:如有错误之处还请各位不吝赐教,一起学习,一起进步。