浅谈 ArrayList 的扩容机制

相信大家对 ArrayList 这个类都不陌生吧,ArrayList 底层是基于数组实现的,作为可变长度的数组用于存储任意类型的对象,那么是否有了解过 ArrayList 是如何实现可变长的呢,它的扩容机制是什么呢?

这篇文章将从源码的角度来讲讲 ArrayList 的扩容机制,有兴趣的读者们可以接着往下了解。

ArrayList 的构造函数

在了解 ArrayList 的扩容机制之前,让我们先看看它的构造函数有哪些?

ArrayList 的字段

// 默认初始容量大小
private static final int DEFAULT_CAPACITY = 10;
// 空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 默认空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存储数据的数组
transient Object[] elementData;
// 元素个数
private int size;
// 最大数组长度
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

相信大家对这里的 EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA 为何需要有两个空数组感到疑惑,这两个空数组之间的区别在什么地方呢?对于这个问题等看完后面的源码后就会有答案了。

ArrayList 的构造函数

// 有参构造函数(initialCapacity:指定的初始化集合大小)
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        // 创建长度为initialCapacity的数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        // 使用空数组(长度为0)
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

// 无参构造器
public ArrayList() {
    // 使用默认空数组(一开始长度为0,等集合被使用后初始化为10)
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 有参构造器(根据指定的集合构造列表)
public ArrayList(Collection<? extends E> c) {
    // 通过toArray()方法转换为数组
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // 数组长度不为0且数组不是Object类型数据则更换类型为Object的新数组
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 数组长度为0则使用空数组
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

通过上面源码中的三种 ArrayList 的构造函数可以看到根据不同的参数构造列表,同时根据不同的参数导致的列表的数组 elementData 被赋予的值也是不同的。

到这里应该可以看出 EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA 的区别:

  • EMPTY_ELEMENTDATA:当构造函数使用指定 initialCapacity 为 0 或指定集合且集合元素为 0 时,会使得 elementData 被赋值为 EMPTY_ELEMENTDATA。这里的空数组表示初始化后的数组长度就是 0 。
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:当构造函数使用无参构造函数时,elementData 被赋值为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。这里的空数组表示开始长度为 0,当列表被使用时,初始化后的数组长度为 10(DEFAULT_CAPACITY),也就是默认数组大小。(通过后面的 add() 源码能够更好理解)

到这里就可以理解为什么 ArrayList 有两个空数组字段,主要是为了设置默认的数组长度,也就是为了区分当前数组是默认数组(也就是还不确定大小,先设置为空数组,等使用后在设置为默认长度),还是已经确认长度为 0 的空数组。

ArrayList 的扩容机制

ArrayList 的扩容机制核心方法是 grow() 方法,这里从 add() 方法进入详细看看 ArrayList 的扩容过程。

扩容过程源码分析

添加元素方法:add()

public boolean add(E e) {
    // size + 1 确保添加后长度足够,进入方法判断是否需要扩容
    ensureCapacityInternal(size + 1);
    // 添加元素
    elementData[size++] = e;
    return true;
}

判断是否是默认长度方法:ensureCapacityInternal()

private void ensureCapacityInternal(int minCapacity) {
    // 数组为默认数组时,minCapacity只会在10之上。
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 进入方法判断是否需要扩容
    ensureExplicitCapacity(minCapacity);
}

判断是否需要扩容方法:ensureExplicitCapacity()

private void ensureExplicitCapacity(int minCapacity) {
    // 用于记录修改次数的计数器,保证多线程下抛出ConcurrentModificationException异常
    modCount++;
    // minCapacity最小需求容量小于当前数组长度时则需要扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

扩容方法:grow()

private void grow(int minCapacity) {
    // 旧容量等于数组长度
    int oldCapacity = elementData.length;
    // 新容量等于数组长度 + 0.5 * 数组长度 也就是 1.5 数组长度
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 新容量还是小于最小需求容量时,直接将新容量赋值为最小需求容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
        
    // 新容量大于MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)调用hugeCapacity()扩容到Integer.MAX_VALUE
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
        
    // 数组拷贝,将数据迁移到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

扩容过程梳理

下面以使用无参构造函数的列表为例,分别讲讲第1个元素插入和第11个元素插入的扩容过程:

添加第1个元素的过程

ArrayList扩容过程.png

  1. 调用 add(e) 方法添加元素,此时的 size=0
  2. 调用 ensureCapacityInternal(1) 方法判断是否是默认长度数组,此时该方法的传参是 size + 1 代表目前列表需要的最小容量。进入该方法后,因为使用的是无参构造器,故这里的数组是默认长度数组,所以 minCapacity 最小容量被置为 10,也就是列表默认长度 DEFAULT_CAPACITY
  3. 调用 ensureExplicitCapacity(10) 方法判断是否需要扩容,此时由于 minCapacity=10 大于 数组长度 elementData.length=0 所以需要扩容。
  4. 调用 grow(10) 方法进行扩容,此时旧数组容量为 oldCapacity=0,新数组容量为 newCapacity=0,而最小容量 minCapacity=10,因为新数组容量小于最小容量,故将新数组容量重新赋值为 newCapacity=minCapacity=10,然后进行数组拷贝,到此完成扩容操作。

添加第11个元素的过程

ArrayList扩容过程2.png

  1. 调用 add(e10) 方法添加元素,此时的 size=10
  2. 调用 ensureCapacityInternal(11) 方法判断是否是默认长度数组,此时该方法的传参是 size + 1 代表目前列表需要的最小容量。进入该方法后,由于已经超出了默认长度 10,所以这里 minCapacity 设置为 11。
  3. 调用 ensureExplicitCapacity(11) 方法判断是否需要扩容,此时由于 minCapacity=11 大于 数组长度 elementData.length=10 所以需要扩容。
  4. 调用 grow(11) 方法进行扩容,此时旧数组容量为 oldCapacity=10,新数组容量为 newCapacity=15,而最小容量 minCapacity=11,因为新数组容量大于最小容量,故将新数组容量依旧为15,然后进行数组拷贝,到此完成扩容操作,新数组的长度扩容到了 15,并将旧数组的元素迁移到新数组中。

以上就是扩容的全过程,扩容公式为 newCapacity=oldCapacity+(oldCapacity>>1)newCapacity = oldCapacity + (oldCapacity >> 1)newCapacity=oldCapacity+(oldCapacity>>1),也就是新容量取值为旧容量的 1.5 倍。

补充:数组拷贝 System.arraycopy() 方法

扩容后的数据迁移调用的是 Arrays.copyOf() 方法底层调用的是 System.arraycopy(),这里简单介绍一下这个常用的方法。

public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

该方法的参数如下:

  • src:源数组
  • srcPos:源数组拷贝的起始位置
  • dest:目标数组
  • destPos:目标数组拷贝的起始位置
  • length:需要拷贝的数组元素的数量

这里作为补充知识简单了解一下即可。

总要有总结

以上就是 ArrayList 的扩容机制的内容了,主要总结如下:

  1. ArrayList 是基于动态数组实现的,可以扩容或缩短(缩短可以手动调用 trimToSize() 方法,ArrayList在元素小于一半长度时会自动缩短长度,这里不作过多说明)数组的大小从而实现可变长的数组。
  2. ArrayList 中声明了 EMPTY_ELEMENTDATADEFAULTCAPACITY_EMPTY_ELEMENTDATA 两个字段,虽然都是空数组,但是两者之间有着不同的作用。
  3. ArrayList 的扩容机制采用的是新数组容量扩容为旧数组容量的 1.5 倍。

补充: ArrayList 提供了 ensureCapacity(int minCapacity) 用于实现手动扩容到指定的容量,这样在需要添加大量数据时可以不必重复进行扩容操作,提高性能。

到这里就是本篇的所有内容了,ArrayList 类中还有许多有意思的方法,有兴趣的读者们可以自行去查看它们的源码。

感谢观看!!!

全部评论

相关推荐

bg:双非本,一段中小厂6个月测开实习今天发这个帖子主要是想聊一聊我秋招以来的一个发展我是在8月底辞职,打算秋招,可是看网上都说金九银十就想着自己就是一个普通本科生,现在九月份都是一些大神在争抢,所以9月份基本上没投,等到了10月份才开始秋招,可是这个时间好像已经有些晚了,今年秋招开启的格外早,提前到了7,8月份,我十月才开始,官网投了很多公司,没有任何一个面试机会,这个情况一直到了十月底才有了第一个面试,当时没有面试经验,所以不出意外的挂了后续就是漫长的投递,但是毫无例外没有面试,没有办法我只能另辟蹊径开始在BOSS上边投递,然后顺便也根据BOSS上边这个公司名称去浏览器搜索看看有没有官网投递渠道,毕竟官网上投递后还是可以第一时间被HR看到的,然后一直不停投递,一开始第一个星期基本上都是投的正式秋招岗位到了第二个星期才开始实习和正式一起投,到十一月底的时候已经沟通了700➕才有一共1个正式的,5个要提前实习的,3个实习的面试,最后结果是过了1个要提前实习的和2个实习的每次面试我都会复盘,发现这些小公司面试官问的五花八门,有的专问基础,有的专问项目,有的啥都问,不过自己也是看出来了一下门道,就是小公司不像大公司面试官那样能力比较强基本上你简历上边的他都会,然后会根据简历来问,小公司面试官他们更多的是看自己会什么,然后看看你简历上边哪些他也是会的然后来问,经过不断的复盘加上背各种各样面试题,到了11月底12月初才有了1个要提前实习的offer还有2个实习的offer,而且薪资待遇对我来说已经很可观了可是啊,人总是这样得了千钱想万钱,我又开始不满现状,但是此时的我面试能力经过这么多面试和复盘已经很强了,然后在十二月份运气爆棚,被极兔和小鹏补录捞起来面试,还有个百度测开的实习面试,这个时候因为有了offer所以感觉有了底气,面试也很自信,最后结果是全部都过了那个时候我感觉自己真的很厉害,我问了极兔那边的HR像我这样的双非本收到offer的在极兔有多少?他告诉我产研岗90%都是硕士,10%里边基本上都是211,985,想我这样的很少很少,那一刻感觉自己超级牛逼,小鹏就更不用说了,最后也是不出意外选择了小鹏所以我就我个人经历想对和我学历履历差不多的牛友一些建议第一:秋招一定要趁早,真到了9,10月,那个时候可能你投的结果可能还不如7,8,11月,第二:最好先拿小公司实习或者正式练练手,提升一下面试能力,我个人觉得因为小公司问的五花八门所以你会更加横向去提升自己能力,而且大公司其实面试没有那么难,除了一些非常卷的岗位,公司大神比较多会问的很难,一般好点的公司都不会问的那么难,他们也知道都是应届生不会要求那么高第三:当有一定能力后,就是坚持了,对于我们这样的学历,没有特别强的履历情况下,就是要抓住提前批和补录的机会,这个时候各方面不会卡的很严,是我们很好很好的一个机会第四:就是运气也是很重要的一部分,不过这个很难去说什么最后祝各位牛友都能收获自己满意的offer😁😁😁
秋招,不懂就问
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务