arraylist 的扩容机制_集合扩容机制

(1) 2024-07-09 17:12

Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说
arraylist 的扩容机制_集合扩容机制,希望能够帮助你!!!。

之前学习Java基础知识的时候了解到ArrayList底层是数组实现的,默认的初始化容量为10,且有自动扩容的机制。

现在通过代码来研究下实现的细节。

ArrayList扩容现象演示

首先通过简单的测试代码看看初始化及扩容现象,测试代码如下:

@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 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第1张
当初始化一个ArrayList后,发现数组的容量为0,并不是默认容量10啊。
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第2张
当向集合中添加完一个元素后,此时数组的容量居然是10了!接着往下看。
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第3张
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第4张
通过循环向集合中添加元素发现:当集合中的元素不超过10的时候,数组的容量一直没有改变。但是当添加第11个元素后,数组的容量变成了15。难道是集合中的元素超过了原来的容量就进行扩容,且每次加5个?再循环一次看看:
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第5张
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第6张
这次当添加了第16个元素后,数组再次扩容,此时容量变成了22。这次容量增加了7。
通过上面的过程目前只知道:创建一个ArrayList时并不是一开始就有默认容量10,扩容机制可能是当添加的元素超过原来的容量时触发,而每次扩容的大小还无法确定。

ArrayList初始化方式

之前演示过程中发现ArrayList初始化时并没有赋予默认的容量10,而是0。

先来看看Arraylist的初始化,通过源码看到有三种初始化方式:

  1. 指定初始化容量
  2. 不指定初始化容量
  3. 使用集合来进行初始化

指定初始化容量的初始化方式

arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第7张
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第8张arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第9张
elementData就是底层存放数据的数组对象。
EMPTY_ELEMENTDATA就是一个容量为0的数组对象。

指定初始化容量的方式进行初始化时有两种情况:

  1. 初始化容量 > 0 时,ArrayList的初始化容量就是指定的容量initialCapacity,底层会创建一个容量为initialCapacity的数组。
  2. 初始化容量 = 0 时,ArrayList的初始化容量就是0,并且将提前定义的空数组赋值给elementData。

简单点说就是指定容量时,ArrayList的容量就是指定的值。

不指定初始化容量的初始化方式

arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第10张
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第11张
DEFAULTCAPACITY_EMPTY_ELEMENTDATA也是一个容量为0的数组,表示默认容量时使用的空数组。

所以,当我们不指定ArrayList的初始化容量时,其初始化容量为0。

使用集合来进行初始化的方式

arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第12张
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第13张
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)); } 

arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第14张
也就是说toArray()方法返回的不一定是一个Object类型的数组。

ArrayList扩容机制

说到扩容,肯定是往集合中添加元素才会发生,所以看看add()方法到底是怎么实现的:
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第15张
点进ensureCapacityInternal()方法:
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第16张
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第17张
minCapacity = size + 1,是指加入一个元素后的集合大小。
DEFAULT_CAPACITY是集合的默认容量,大小为10。终于见到所谓的默认初始化容量了。
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第18张
calculateCapacity()方法看名字就是计算容量的意思。如果当前内部的数组是没有指定初始化容量时赋值的对象,那么取DEFAULT_CAPACITY和minCapacity两者中较大的为新的集合大小。
(当然,向一个没有指定初始化容量的ArrayList中添加第一个元素时,minCapacity=1,所以这时会取默认大小为10。)
但是这里还没到扩容的实现,所以也还不是新集合的容量,姑且只能称为集合的大小。
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第19张
ensureExplicitCapacity()方法就是确定容量。首先修改modCount的值,然后看到一个关键的判断条件:

minCapaticy - elementData.length > 0

这就是触发扩容机制的条件:添加元素后集合的大小(size)超过原来数组的大小(length)

再点进grow()方法看看,总该到扩容的实现了吧:
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第20张
首先获取原来数组的length赋值给oldCapacity,也就是集合原来的容量。

与之相对,newCapacity就是指集合新的容量。(所以之前的minCapacity只是集合的大小而不是容量)

关键的扩容就是这一句:

newCapacity = oldCapacity + (oldCapacity >> 1)

说明新的集合容量为原来的1.5倍,就是说ArrayList的扩容因子为1.5。

这里还对newCapacity做了两次判断:

  1. 如果newCapacity < minCapacity。新的容量取minCapacity。
    arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第21张
    arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第22张
  2. 如果newCapacity > MAX_ARRAY_SIZE。扩容后的容量如果超过了数组的最大容量且超过了整型数的最大值,则新的容量取整型数的最大值;如果只是超过了数组的最大容量,那么新的容量就取数组的最大容量。

最后创建新的数组,然后将元素添加到集合中。addAll()方法的执行流程和add()方法是差不多的,不再赘述。

扩容因子为什么是1.5

参考: 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,新容量刚刚好永远大于过去所有废弃的数组容量。

  • 为什么不取扩容固定容量呢?
    扩容的目的需要综合考虑这两种情况:
  1. 扩容容量不能太小,防止频繁扩容,频繁申请内存空间 + 数组频繁复制
  2. 扩容容量不能太大,需要充分利用空间,避免浪费过多空间;

而扩容固定容量,很难决定到底取多少值合适,取任何具体值都不太合适,因为所需数据量往往由数组的客户端在具体应用场景决定。依赖于当前已经使用的量 * 系数, 比较符合实际应用场景。
比如,我现在已经用到一个数组100的容量,接下来很可能会有这个数量级的数据需要插入。

  • 为什么是1.5,而不是1.2,1.25,1.8或者1.75?
    因为1.5 可以充分利用移位操作,减少浮点数或者运算时间和运算次数。

    // 新容量计算 int newCapacity = oldCapacity + (oldCapacity >> 1); 

总结

  1. jdk1.8之后“ArrayList的默认初始化容量是10”,但是是在添加第一个元素时才真正将容量赋值为10。而在jdk1.7中默认初始化容量确实是10。
  2. ArrayList的扩容机制在什么时候触发?是在集合中的元素超过原来的容量时触发。
  3. ArrayList的扩容因子是1.5,扩容为原来的1.5倍。

附一张简易的流程图:
arraylist 的扩容机制_集合扩容机制_https://bianchenghao6.com/blog__第23张

今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。

上一篇

已是最后文章

下一篇

已是最新文章

发表回复

相关推荐