Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说
arraylist 的扩容机制_集合扩容机制,希望能够帮助你!!!。
之前学习Java基础知识的时候了解到ArrayList底层是数组实现的,默认的初始化容量为10,且有自动扩容的机制。
现在通过代码来研究下实现的细节。
首先通过简单的测试代码看看初始化及扩容现象,测试代码如下:
@Test public void test3() {
List<Integer> list = new ArrayList<>(); list.add(0); System.out.println("************************"); for (int i = 1; i < 11; i++) {
list.add(i); } System.out.println("************************"); for (int j = 11; j < 16; j++) {
list.add(j); } System.out.println("***********end************"); }
接下来开始打断点,debug代码。
当初始化一个ArrayList后,发现数组的容量为0,并不是默认容量10啊。
当向集合中添加完一个元素后,此时数组的容量居然是10了!接着往下看。
通过循环向集合中添加元素发现:当集合中的元素不超过10的时候,数组的容量一直没有改变。但是当添加第11个元素后,数组的容量变成了15。难道是集合中的元素超过了原来的容量就进行扩容,且每次加5个?再循环一次看看:
这次当添加了第16个元素后,数组再次扩容,此时容量变成了22。这次容量增加了7。
通过上面的过程目前只知道:创建一个ArrayList时并不是一开始就有默认容量10,扩容机制可能是当添加的元素超过原来的容量时触发,而每次扩容的大小还无法确定。
之前演示过程中发现ArrayList初始化时并没有赋予默认的容量10,而是0。
先来看看Arraylist的初始化,通过源码看到有三种初始化方式:
指定初始化容量的初始化方式
elementData
就是底层存放数据的数组对象。
EMPTY_ELEMENTDATA
就是一个容量为0的数组对象。
指定初始化容量的方式进行初始化时有两种情况:
简单点说就是指定容量时,ArrayList的容量就是指定的值。
不指定初始化容量的初始化方式
DEFAULTCAPACITY_EMPTY_ELEMENTDATA
也是一个容量为0的数组,表示默认容量时使用的空数组。
所以,当我们不指定ArrayList的初始化容量时,其初始化容量为0。
使用集合来进行初始化的方式
size
指的是集合中元素的个数,也就是集合的大小(size不是容量)。
使用集合来进行初始化时,传入的参数为Collection
的子类。使用toArray()
方法将其转换为数组然后赋值给elementData
。
注意:此时的element.length
就是集合中存在的元素个数。
当集合(c)中不存在元素时,会给将EMPTY_ELEMENTDATA
赋值给elementdata
。
这里有个疑问:c.toArray might (incorrectly) not return Object[] (see )这句话是啥情况?
从网上找到个例子
https://www.cnblogs.com/gilbertbright/p/11714334.html,是这样的:
@Test public void test4() {
Integer[] array = {
1, 2}; //通过Arrays转换成的List,保留了原本的类型 List list = Arrays.asList(array); //即使再将其转换为Object类型的数组,还是原本的类型 Object[] array1 = list.toArray(); System.out.println("通过数组转换:" + (array1.getClass() == Object[].class)); // 如果是创建的集合,则类型可以转换 List<Integer> list1 = new ArrayList<Integer>(); System.out.println("通过集合转换:" + (list1.toArray().getClass() == Object[].class)); }
也就是说toArray()方法返回的不一定是一个Object类型的数组。
说到扩容,肯定是往集合中添加元素才会发生,所以看看add()方法到底是怎么实现的:
点进ensureCapacityInternal()方法:
minCapacity = size + 1,是指加入一个元素后的集合大小。
DEFAULT_CAPACITY是集合的默认容量,大小为10。终于见到所谓的默认初始化容量了。
calculateCapacity()方法看名字就是计算容量的意思。如果当前内部的数组是没有指定初始化容量时赋值的对象,那么取DEFAULT_CAPACITY和minCapacity两者中较大的为新的集合大小。
(当然,向一个没有指定初始化容量的ArrayList中添加第一个元素时,minCapacity=1,所以这时会取默认大小为10。)
但是这里还没到扩容的实现,所以也还不是新集合的容量,姑且只能称为集合的大小。
ensureExplicitCapacity()方法就是确定容量。首先修改modCount的值,然后看到一个关键的判断条件:
minCapaticy - elementData.length > 0
这就是触发扩容机制的条件:添加元素后集合的大小(size)超过原来数组的大小(length)。
再点进grow()方法看看,总该到扩容的实现了吧:
首先获取原来数组的length赋值给oldCapacity,也就是集合原来的容量。
与之相对,newCapacity就是指集合新的容量。(所以之前的minCapacity只是集合的大小而不是容量)
关键的扩容就是这一句:
newCapacity = oldCapacity + (oldCapacity >> 1)
说明新的集合容量为原来的1.5倍,就是说ArrayList的扩容因子为1.5。
这里还对newCapacity做了两次判断:
最后创建新的数组,然后将元素添加到集合中。addAll()方法的执行流程和add()方法是差不多的,不再赘述。
参考: https://www.cnblogs.com/fortunely/p/14279231.html.
扩容因子最适合范围为(1, 2)。
下面举一组对比的例子,取不同扩容因子和初始容量的内存分配情况,当然容量不可能是4,只是方便说明:
k = 2, capacity = 4 0123 0 0 0 0123... k = 1.5, capacity = 4 0123 012345 0 <--(01112) 01112 ...
k=1.5时,就能充分利用前面已经释放的空间。如果k >= 2,新容量刚刚好永远大于过去所有废弃的数组容量。
而扩容固定容量,很难决定到底取多少值合适,取任何具体值都不太合适,因为所需数据量往往由数组的客户端在具体应用场景决定。依赖于当前已经使用的量 * 系数, 比较符合实际应用场景。
比如,我现在已经用到一个数组100的容量,接下来很可能会有这个数量级的数据需要插入。
为什么是1.5,而不是1.2,1.25,1.8或者1.75?
因为1.5 可以充分利用移位操作,减少浮点数或者运算时间和运算次数。
// 新容量计算 int newCapacity = oldCapacity + (oldCapacity >> 1);
附一张简易的流程图:
今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
上一篇
已是最后文章
下一篇
已是最新文章