ArrayList扩容机制分析

ArrayList底层数据结构就是数组,用数组来存储元素。既然是用到数组,就涉及到扩容的问题。

  • size变量是实际存储元素的个数,项目代码中通常用ArrayList.size()方法来获取元素的个数。
  • 数组容量,是指存储元素的数组的长度:elementData.length

3个构造方法

java 复制代码
/**
 * 默认初始容量大小
 */
private static final int DEFAULT_CAPACITY = 10;

private static final Object[] EMPTY_ELEMENTDATA = {};

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

/**
 * 默认构造函数,使用初始容量10构造一个空列表(无参数构造)
 * 如果不往里面add元素,实际列表的容量是0
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
 * 带初始容量参数的构造函数。(用户自己指定容量)
 */
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {//初始容量大于0
        //创建initialCapacity大小的数组
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {//初始容量等于0
        //创建空数组
        this.elementData = EMPTY_ELEMENTDATA;
    } else {//初始容量小于0,抛出异常
        throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
    }
}


/**
 * 构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
 * 如果指定的集合为null,throws NullPointerException。
 */
public ArrayList(Collection<? extends E> c) {
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // c.toArray might (incorrectly) not return Object[] (see 6260652)
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // replace with empty array.
        this.elementData = EMPTY_ELEMENTDATA;
    }
}

以无参数构造方法创建ArrayList时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩为 10 (如果是调用有参构造方法创建ArrayList,debug之后看了初始化赋值的也是一个空数组,数组容量并不是传入的大小。?)

扩容入口:add()方法

java 复制代码
public boolean add(E e) {
    // 加元素之前,先调用ensureCapacityInternal方法,
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    // 这里看到ArrayList添加元素的实质就相当于为数组赋值
    elementData[size++] = e;
    return true;
}

判断是否扩容:ensureCapacityInternal()方法

上述add方法调用的ensureCapacityInternal()方法如下

java 复制代码
// 直接调用calculateCapacity和ensureExplicitCapacity方法
private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 根据给定的最小容量和当前数组元素来计算所需容量。
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    // 满足该条件,即为初始数组为空的情况。此时所需容量取默认容量和最小容量中的最大值
    // 初始情况下,minCapacity总是1,所以首次计算的容量是10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

// 判断当前数组容量是否足以存储上述计算出来的minCapacity个元素。
// 如果当前数组容量不够,则需要调用grow方法,进行扩容。
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // 如果当前数组大小,小于minCapacity,则需要进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

判断是否扩容流程图

总结

  • 若调用无参的构造方法进行初始化,则在添加第1个元素的时候需要进行一次扩容,进入grow方法
  • 当add第2个元素时,minCapacity为2(size+1),此时elementData.length(数组容量)在添加第1个元素后扩容成10了,所以此时不会进入grow方法。同理添加第3、4、...10个元素时,都不会触发扩容。
  • 添加第11个元素的时候,minCapacity为11,比elementData.length(为10)要大,所以需要扩容

扩容的步骤:grow()方法

java 复制代码
// 核心思想是:创建一个更大的新数组,将老数组的元素复制到新数组中。

/**
 * 要分配的最大数组大小
 */
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

private void grow(int minCapacity) {
    // overflow-conscious code
    // oldCapacity为旧容量,newCapacity为新容量。
    int oldCapacity = elementData.length;
    // 将oldCapacity右移一位,相当于oldCapacity/2。
    // 计算结果为:newCapacity为oldCapacity的1.5倍。
    // ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右(oldCapacity 为偶数就是 1.5 倍,否则是 1.5 倍左右)!
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    
    // 若计算的新容量仍小于最小需要的容量,则将新容量赋值为最小需要的容量。
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
        
    // 如果新容量大于最大数组容量,则调用hugeCapacity方法比较 minCapacity 和 MAX_ARRAY_SIZE,规则如下:
    // 如果minCapacity大于最大容量,则新容量为Integer.MAX_VALUE;
    // 否则,新容量大小为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}
  • add 第 1 个元素时,oldCapacity 为 0,经比较后第一个 if 判断成立,newCapacity = minCapacity(为 10)。但是第二个 if 判断不会成立,即 newCapacity 不比 MAX_ARRAY_SIZE 大,则不会进入 hugeCapacity 方法。数组容量为 10,add 方法中 return true,size 增为 1。

  • add 第 11 个元素进入 grow 方法时,newCapacity 为 15,比 minCapacity(为 11)大,第一个 if 判断不成立。新容量没有大于数组最大 size,不会进入 hugeCapacity 方法。数组容量扩为 15,add 方法中 return true,size 增为 11。

  • 以此类推······

这里补充一点比较重要,但是容易被忽视掉的知识点:

  • Java 中的 length属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性。
  • Java 中的 length() 方法是针对字符串说的,如果想看这个字符串的长度则用到 length() 这个方法。
  • Java 中的 size() 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看。

扩容步骤流程图

参考文章

  1. JavaGuide面试题
相关推荐
测试老哥6 分钟前
外包干了两年,技术退步明显。。。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
初晴~1 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
盖世英雄酱581361 小时前
InnoDB 的页分裂和页合并
数据库·后端
小_太_阳2 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
直裾2 小时前
scala借阅图书保存记录(三)
开发语言·后端·scala
ThisIsClark2 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
星就前端叭2 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
小林coding3 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云
AI理性派思考者3 小时前
【保姆教程】手把手教你在Linux系统搭建早期alpha项目cysic的验证者&证明者
后端·github·gpu
测试19984 小时前
外包干了2年,技术退步明显....
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展