记ArrayList的扩容过程

本文深入剖析了ArrayList的内部实现机制,包括其构造方法、扩容策略及元素添加过程。详细介绍了ArrayList如何在不同情况下调整其容量,以确保高效的数据存储和访问。

 ArrayList简单介绍

ArrayList是一个底层用数组实现的集合,支持随机访问,元素有序并且可以重复。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

①实现RandomAccess接口

这是一个标记接口,一般此标记用于List实现,表明支持快速的随机访问。

②实现Cloneable接口

也是一个标记接口,接口内无任何方法体和常量的声明,如果想克隆对象,必须实现Cloneable接口,表明该类是可以被克隆的。

③实现Serializable接口

也是标记接口,表示能被序列化。

④实现List接口

里面很多必须实现的方法,例如:size(),isEmpty().....

 

研究第一步:

先直接new一个ArrayList,向集合添加11个元素,为什么是11个等下解析

public class TestArrayList {
    public static void main(String[] args) {
        List<String> stringList = new ArrayList<>();
        stringList.add("1");
        stringList.add("2");
        stringList.add("3");
        stringList.add("4");
        stringList.add("5");
        stringList.add("6");
        stringList.add("7");
        stringList.add("8");
        stringList.add("9");
        stringList.add("10");
        stringList.add("11");
    }
}

从List<String> stringList = new ArrayList<>();可以看出我调用的是ArrayList的无参构造方法

研究第二步:

打开ArrayList源码,查看字段属性

 
      //默认容量10
    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;

其实默认容量不是调用构造方法的时候就为集合创建了的,这个时候我们要先看看构造函数,构造函数有三种

第一种无参构造,就是我们上面测试代码要调用的构造函数

   public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

当我们调用无参构造函数的时候,就将真正存放集合数据的数组设置成了DEFAULTCAPACITY_EMPTY_ELEMENTDATA,而DEFAULTCAPACITY_EMPTY_ELEMENTDATA就是一个Object类型的空数组,里面什么都没有,也就是size=0。

第二种构造方法:

 //initialCapacity代表你想要创建的集合的大小
public ArrayList(int initialCapacity) {
        //先判断这个大小是否大于0
        if (initialCapacity > 0) {
            //大于0的时候就直接创建你想要的大小的Object类型的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //等于0时就还是创建一个空数组
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //大小是负数则抛异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

第三种构造方法:

//这是将已有的集合复制到ArrayList中去
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;
        }
    }

在调用完无参构造函数之后,我们知道的是,我们通过new ArrayList<>()创建了一个空的数组,里面什么都没有,size=0,接着我们就要往该集合里面添加元素了,我们执行了add(E e)方法(添加方法有两个,这里是调用了一个参数的我们就直接解析一个参数的add方法吧)。

研究第三步:

我们查看源代码add(E e)方法的实现

   public boolean add(E e) {
        //检测是否需要扩容
        ensureCapacityInternal(size + 1);  
        //赋值操作,因为已经执行完扩容了的,长度加了1
        /**
         * elementData[size++] = e;
         * 相当于elementData[size] = e;
         *       size++;
         * */
        elementData[size++] = e;
        return true;
    }

add方法里面调用了ensureCapacityInternal()方法,因为我们之前调用无参构造的时候创建的是空数组,size=0,所以现在ensureCapacityInternal方法的参数值为0+1=1.

接着查看ensureCapacityInternal方法的实现

 //扩容入口
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

这里elementData就是空数组(因为上面调用无参构造的时候给elementData赋值就是空数组), minCapacity就是1(是前面的size+1得到的结果)

这里继续调用calculateCapacity()方法,查看calculateCapacity方法的实现

 //计算容量应该是多少
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //当前的数组是否是默认容量的数组(在调用无参构造函数的时候创建的就是默认容量的数组)
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //把默认容量10与minCapacity比较返回较大的(minCapacity就是当前数组的长度+1了的)
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //不是默认容量数组那么就直接返回minCapacity
        return minCapacity;
    }

因为我传过来的elementData就是空数组,minCapacity就是1,所以在if判断的时候就将DEFAULT_CAPACITY值给返回了,这里才动用到了默认值10.所以这时候ensureExplicitCapacity(calculateCapacity(elementData, minCapacity))就等价于ensureExplicitCapacity(10)

查看ensureExplicitCapacity方法的实现:

private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        //最小需要的容量大于当前数组的大小的时候就要进行扩容了
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

这函数里面的minCapacity=10,因为elementData还是个空数组,长度为0所以在if判断的时候就要去调用grow(10)方法了,调用grow方法就是进行真正的扩容了

查看grow方法的实现

 private void grow(int minCapacity) {

        //1、先记录原来数组的容量
        int oldCapacity = elementData.length;

        //2、右移一位就相当于除2,那么新的容量=旧的容量+旧的容量/2
        //也就是新的容量是原来的1.5倍了
        int newCapacity = oldCapacity + (oldCapacity >> 1);

        //3、新的容量还是小于你最小的容量的时候就是说明你扩容之后还是不够
        if (newCapacity - minCapacity < 0)

            //4、直接把你最小的容量给新的容量
            newCapacity = minCapacity;

        //5、如果你扩容的容量已经大于最大值了
        if (newCapacity - MAX_ARRAY_SIZE > 0)

            //6、给新容量一个最大值
            newCapacity = hugeCapacity(minCapacity);

        //7、把原来的数据拷贝,把新的容量给新的数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

在1时,因为目前elementData是空的,所以oldCapacity=0

在2时:newCapacity 还是等于0

在3时:0-10 < 0,进入4,所以新的容量现在被赋值为10了

(当程序执行5的时候就数组大小不断扩容之后比最大值还要大的时候才执行的)

紧接着执行7:将这个空数组进行拷贝到一个容量为10的数组中去了,所以执行到这里的时候真实的数组现在容量是10了,只要数组的数据还没有达到10的时候都不需要再扩容了,这也就是为什么一开始的时候是添加了11个数据,就是因为第11个数据想要存的时候,这时候又要去扩容了。

执行完7之后elementData就是一个容量为10的数组了,这时候grow方法执行完成了,回到ensureExplicitCapacity方法,他也执行完了,再回到ensureCapacityInternal方法,也执行完了,最后回到了add方法,执行elementData[size++] = e;这个时候才将元素添加进来,添加了一个元素size就变成了1,最后返回true表示添加成功。

 

对于ArrayList集合添加元素最后总结一下:

①、当通过 ArrayList() 构造一个空集合,初始长度是为0的,第 1 次添加元素,会创建一个长度为10的数组,并将该元素赋值到数组的第一个位置。

  ②、第 2 次添加元素,集合不为空,而且由于集合的长度size+1是小于数组的长度10,所以直接添加元素到数组的第二个位置,不用扩容。

  ③、第 11 次添加元素,此时 size+1 = 11,而数组长度是10,这时候创建一个长度为10+10*0.5 = 15 的数组(扩容1.5倍),然后将原数组元素引用拷贝到新数组。并将第 11 次添加的元素赋值到新数组下标为10的位置。

  ④、第 Integer.MAX_VALUE - 8 = 2147483639,然后 2147483639%1.5=1431655759(这个数是要进行扩容) 次添加元素,为了防止溢出,此时会直接创建一个 1431655759+1 大小的数组,这样一直,每次添加一个元素,都只扩大一个范围。

  ⑤、第 Integer.MAX_VALUE - 7 次添加元素时,创建一个大小为 Integer.MAX_VALUE 的数组,在进行元素添加。

  ⑥、第 Integer.MAX_VALUE + 1 次添加元素时,抛出 OutOfMemoryError 异常。

  注意:能向集合中添加 null 的,因为数组可以有 null 值存在。

<think>嗯,用户让我介绍一下ArrayList扩容机制。首先,我需要回忆一下ArrayList的基本概念。ArrayList是Java中常用的动态数组,基于数组实现,可以动态调整容量。用户可能已经对ArrayList有基本了解,但想深入了解其扩容机制,可能在面试准备或者优化代码时遇到了问题。 接下来,我需要确定用户的需求层次。他们可能需要知道扩容的具体步骤、源码分析、扩容因子以及性能影响。可能还有对比其他集合比如Vector或者LinkedList的区别。不过问题明确提到扩容机制,所以应专注于ArrayList内部如何扩容。 首先,回顾ArrayList的结构。它内部有一个Object数组elementData来存储元素,size表示当前元素数量。默认初始容量是10,但也可以自定义初始容量。当添加元素时,如果数组已满,就会触发扩容。 那么扩容的具体步骤是怎样的呢?当调用add()方法时,会先检查是否需要扩容。ensureCapacityInternal方法会计算最小需要容量,比如当前size+1。然后比较是否需要扩容,如果需要,就执行grow方法。 在grow方法中,新容量通常是旧容量的1.5倍。通过位运算oldCapacity + (oldCapacity >> 1)来计算。如果这个新容量还不够,比如addAll添加了多个元素,此时会直接用所需的最小容量。但新容量不能超过Integer.MAX_VALUE,否则会报OutOfMemoryError。 接下来要考虑的是扩容的性能问题。因为每次扩容都需要复制原数组到新数组,这个操作的时间复杂度是O(n),频繁添加元素可能导致性能下降。因此,如果事先知道数据量较大,最好在创建ArrayList时指定初始容量,减少扩容次数。 可能用户还关心与Vector的区别。Vector的扩容是增长为原来的2倍,而ArrayList是1.5倍。另外,Vector是线程安全的,但ArrayList不是,所以性能更好。 是否需要分析源码呢?用户的问题提到“分析描述”,可能希望有源码层面的解释。比如,在Java 8中的grow方法实现,以及ensureExplicitCapacity的判断逻辑。不过可能用户不需要逐行分析,而是关键点的说明。 另外,可能需要注意JDK版本的变化。比如,在更早的版本中,初始容量的计算可能有所不同,但通常默认是10。用户可能在使用不同版本时需要注意这一点。 还要提到扩容的触发条件,比如每次添加元素时,如果当前size+1超过数组长度,就会触发。如果是addAll,可能一次性增加多个元素,这时候需要计算足够的容量,可能直接扩容到刚好满足或者1.5倍中的较大者。 然后,举一个例子说明扩容过程。比如初始容量为10,当添加第11个元素时,容量变为15。再添加到16时,扩容到22,依此类推。这样的例子能帮助用户更直观地理解。 最后,总结一下优化建议,比如预估数据量来设置初始容量,避免多次扩容带来的开销。同时,可以指出为什么ArrayList扩容因子是1.5,可能是在时间和空间效率上的权衡,但不同语言或库可能有不同的策略,比如Python的list过度分配策略。 需要检查是否有遗漏的关键点,比如modCount的处理,或者迭代时并发修改的问题,但这些可能属于并发处理的范畴,不是扩容机制的核心内容。所以重点还是扩容的步骤、计算方式、源码关键方法和性能影响。 可能用户还关心扩容时的内存分配,这涉及到数组的复制,使用Arrays.copyOf,底层调用System.arraycopy,这是一个本地方法,效率较高,但依然有成本。因此,频繁扩容应尽量避免。 总结下来,回答的结构应该是先介绍ArrayList的结构,然后详细描述扩容的触发条件和步骤,包括源码分析,接着讨论性能影响和优化建议,最后可能的对比和示例。确保用中文清晰表达,避免技术术语使用不当,必要时举例说明。</think>ArrayList 是 Java 中基于动态数组实现的集合类,其**扩容机制**是其核心特性之一,能够在元素数量超过当前容量时自动扩展存储空间。以下是其扩容机制的详细分析: --- ### **1. 底层数据结构** - ArrayList 内部通过一个 `Object[] elementData` 数组存储元素,`size` 属性录实际元素数量。 - 默认初始容量为 **10**(通过 `DEFAULT_CAPACITY` 定义),但若构造时指定初始容量或添加第一个元素时初始化,容量可能不同。 --- ### **2. 扩容触发条件** 当调用 `add()` 方法添加元素时,会先检查容量是否足够: ```java public boolean add(E e) { ensureCapacityInternal(size + 1); // 确保容量足够 elementData[size++] = e; return true; } ``` - **容量不足时触发扩容**:若当前元素数量 `size + 1` 超过数组长度 `elementData.length`,则触发扩容。 --- ### **3. 扩容流程分析** #### **(1) 计算最小所需容量** 通过 `ensureCapacityInternal()` 方法确定最小需要容量: ```java private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } ``` #### **(2) 判断是否需要扩容** ```java private void ensureExplicitCapacity(int minCapacity) { modCount++; if (minCapacity - elementData.length > 0) grow(minCapacity); // 执行扩容 } ``` #### **(3) 扩容核心方法 `grow()`** ```java private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量 = 旧容量 * 1.5 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 处理极端情况(如 addAll 大量元素) if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 处理最大容量限制(Integer.MAX_VALUE - 8) elementData = Arrays.copyOf(elementData, newCapacity); // 数据复制到新数组 } ``` - **扩容公式**:新容量通常是旧容量的 **1.5 倍**(通过位运算 `oldCapacity >> 1` 实现)。 - **特殊处理**:若 1.5 倍仍不足,则直接使用所需的最小容量 `minCapacity`。 - **容量上限**:最大容量为 `Integer.MAX_VALUE - 8`,超过可能抛出 `OutOfMemoryError`。 --- ### **4. 性能影响** - **时间复杂度**:每次扩容需复制原数组到新数组,时间复杂度为 **O(n)**。 - **空间浪费**:扩容后可能有未使用的空间(如从 10 扩容到 15,但只用了 11 个位置)。 - **优化建议**:若已知数据量较大,可通过 `ArrayList(int initialCapacity)` 指定初始容量,减少扩容次数。 --- ### **5. 示例说明** - **初始容量 10**,添加第 11 个元素时触发扩容: ``` 旧容量:10 → 新容量:15 ``` - 继续添加到第 16 个元素时再次扩容: ``` 旧容量:15 → 新容量:22(15 + 15/2 = 22.5,取整为 22) ``` --- ### **6. 对比 Vector 的扩容** - **Vector** 默认扩容为原来的 **2 倍**(可通过构造函数调整增量),且是线程安全的。 - **ArrayList** 扩容为 **1.5 倍**,非线程安全但性能更高。 --- ### **总结** ArrayList扩容机制通过动态扩展数组容量实现高效的元素管理,但频繁扩容会带来性能损耗。**预分配合理初始容量**是优化关键。理解其机制有助于在实际开发中合理选择数据结构,避免性能瓶颈。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值