细品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后,发现数组的容量为0,并不是默认容量10啊。

当向集合中添加完一个元素后,此时数组的容量居然是10了!接着往下看。


通过循环向集合中添加元素发现:当集合中的元素不超过10的时候,数组的容量一直没有改变。但是当添加第11个元素后,数组的容量变成了15。难道是集合中的元素超过了原来的容量就进行扩容,且每次加5个?再循环一次看看:


这次当添加了第16个元素后,数组再次扩容,此时容量变成了22。这次容量增加了7。
通过上面的过程目前只知道:创建一个ArrayList时并不是一开始就有默认容量10,扩容机制可能是当添加的元素超过原来的容量时触发,而每次扩容的大小还无法确定。



ArrayList初始化方式

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

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

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

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


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rS0YjROJ-1649596812161)(G:\Typora\Java源码学习.asserts\image-20220410193350095.png)]
elementData就是底层存放数据的数组对象。
EMPTY_ELEMENTDATA就是一个容量为0的数组对象。

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

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

简单点说就是指定容量时,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 6260652)这句话是啥情况?

从网上找到个例子
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));
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xIQWNH8R-1649596812162)(G:\Typora\Java源码学习.asserts\image-20220410200419769.png)]
也就是说toArray()方法返回的不一定是一个Object类型的数组。



ArrayList扩容机制

说到扩容,肯定是往集合中添加元素才会发生,所以看看add()方法到底是怎么实现的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-StVxCfRq-1649596812162)(G:\Typora\Java源码学习.asserts\image-20220410201352520.png)]
点进ensureCapacityInternal()方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O7tKlEPk-1649596812163)(G:\Typora\Java源码学习.asserts\image-20220410201455934.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gK3dhC4q-1649596812163)(G:\Typora\Java源码学习.asserts\image-20220410201722109.png)]
minCapacity = size + 1,是指加入一个元素后的集合大小。
DEFAULT_CAPACITY是集合的默认容量,大小为10。终于见到所谓的默认初始化容量了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0NOjXoC1-1649596812164)(G:\Typora\Java源码学习.asserts\image-20220410202130849.png)]
calculateCapacity()方法看名字就是计算容量的意思。如果当前内部的数组是没有指定初始化容量时赋值的对象,那么取DEFAULT_CAPACITY和minCapacity两者中较大的为新的集合大小。
(当然,向一个没有指定初始化容量的ArrayList中添加第一个元素时,minCapacity=1,所以这时会取默认大小为10。)
但是这里还没到扩容的实现,所以也还不是新集合的容量,姑且只能称为集合的大小。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IpacVWYU-1649596812164)(G:\Typora\Java源码学习.asserts\image-20220410201506388.png)]
ensureExplicitCapacity()方法就是确定容量。首先修改modCount的值,然后看到一个关键的判断条件:

minCapaticy - elementData.length > 0

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

再点进grow()方法看看,总该到扩容的实现了吧:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TlI7iGSD-1649596812165)(G:\Typora\Java源码学习.asserts\image-20220410203010746.png)]
首先获取原来数组的length赋值给oldCapacity,也就是集合原来的容量。

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

关键的扩容就是这一句:

newCapacity = oldCapacity + (oldCapacity >> 1)

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

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

  1. 如果newCapacity < minCapacity。新的容量取minCapacity。
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-reyLC11h-1649596812165)(G:\Typora\Java源码学习.asserts\image-20220410203742188.png)]
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7iMVSYiX-1649596812166)(G:\Typora\Java源码学习.asserts\image-20220410203946434.png)]
  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
    01234567
            0123456789101112131415
                                  012345678910111213141516171819202122232425262728293031
                                                                                        0123...

k = 1.5, capacity = 4
0123
    012345
          012345678
                <--(0123456789101112)
0123456789101112
                ...

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倍。

附一张简易的流程图:

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值