六千字详解!一篇看懂 ArrayList 的扩容机制(完整源码解析)

☀️今天花了很久写了这篇关于 ArrayList 扩容机制源码解析的博客,在阅读源码的过程中发现了很多之前有误解的地方,也加深了对代码的理解,所以写下了这篇博客。
🎶本文附带了流程中所有的代码和附加解析,我有信心一定能帮大家完整的梳理和认识整个流程,如果博客对你有帮助的话别忘了留下你的点赞和关注💖💖💖

ArrayList 底层结构和源码分析

01.整体把握

这里首先列出 ArrayList 扩容的几个特点,看完这些特点再去阅读体验会比较好

1)ArrayList 中维护了一个 Object 类型的数组,elementData。

2)当每次创建 ArrayList 对象的时候,如果使用的是无参构造器,则初始的 elementData 的容量为 0,第一次添加的时候则扩容 elementData 为 10,如果需要再次扩容,则扩容为原来的 1.5 倍

3)如果使用的是指定大小的构造器,则初始的 elementData 的容量就是指定的大小,如果需要扩容,也是直接扩容为 elementData 的 1.5 倍。


02.无参构造方法

下面来看具体的源码,首先就是 ArrayList 的无参构造方法:

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

解析:可以看到,它将 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 赋值给了 elementData;ArrayList 的内部实现就是基于这个 elementData 数组,它是 ArrayList 存放元素的位置,之后的拿取、扩容之类的操作本质上都是在操纵它。

DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是一个常量,来看一下它的定义:

	/**
     * Shared empty array instance used for default sized empty instances. We
     * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

这是一个共享的(static)空数组实例,被用作使用 默认无参构造方法 时,elementData 的默认值;它的作用是与另一个共享的空数组实例来做区分,那另一个共享数组为 EMPTY_ELEMENTDATA,它在接下来要讲的 有参构造方法 中具有很重要的作用;简单来说 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 是标识着通过无参构造形成的空 ArrayList,而 EMPTY_ELEMENTDATA 是通过有参构造形成的空 ArrayList,它们在后续的扩容策略中会有所不同。

除了这个标志作用以外,它们的作用都是相同的,都是在内存中开辟了一个 共享 空间用来存放空数组,可以避免过早的为新创建的 ArrayList 实例分配内存,达到节省内存的作用。


03.有参构造方法

1)先观察 ArrayList 的有参构造方法,ArrayList 其实提供了两种有参构造方法,通过 CTRL + P 快捷键,可以查看其中的参数:

在这里插入图片描述

可以看到有参构造的第一种方式就是提供一个 int 类型的 initialCapacity,也就是初始的容量;第二种方法是提供一个集合类,构造方法会将这个集合类转为 ArrayList 的类型。

先来看指定初始容量的构造方法,这里插嘴一句,如果大家用的 idea 版本是新版的,可以多去使用那个 SmartStepInto,是调试器提供的一个智能步入的方式,可以智能的跳过一些不必要的步骤;说回到这个有参构造方法

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

如果指定的初始容量大于零,就直接构造一个容量为初始容量的 Object 数组,然后将其赋值给 elementData;否则,当初始的容量等于 0 的时候,就指定其为 EMPTY_ELEMENTDATA!这里就能看出与无参构造的区别了;如果是其他的数字,比如负数,就抛出一个异常。

2)再来看给定 Collection 的构造方法:

    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }

解析:首先通过 Collction 接口中定义的 toArray() 方法,将集合类转为一个数组,然后指定 size(当前 ArrayList 实例中存放元素的个数)赋值成数组的长度,然后判断这个长度是否为 0,如果不为 0 就将其赋值给 elementData。

第一个 if 的 else 中的 elementData = Arrays.copyOf(a, size, Object[].class); 是为了保证 ArrayList 中维护的始终是一个 Object 数组,因为得到的 a 数组是通过原本集合类的 toArray() 方法得到的,不能保证返回的是一个 Object 数组,此时就会出现问题了,比如看下面这个案例

public class Main {
    public static void main(String[] args) {
        Dog dog1 = new Dog();
        Dog dog2 = new Dog();
        Dog[] dogs = new Dog[]{dog1, dog2};
        Object[] a = dogs;
        System.out.println(a[0] instanceof Dog);;
        System.out.println(a[0].equals(1));
    }
}
class Dog {
    @Override
    public boolean equals(Object obj) {
        System.out.println("我的equals方法");
        return false;
    }
}

比如说此时返回的是一个 Dog 数组,我使用语句将其转变为 Object 数组,但数组中元素的类型其实是未改变的,通过 instance of 仍然可以观察到运行的类型是 Dog,此时如果我调用 a[0] 的 equals 方法其实调用的就是重写的方法:

true
我的equals方法
false

然后如果在执行中使用到了这些重写的代码就会导致很多问题,其实,使用源码中提供的类大概率不会出现如上的问题,但如果是我们自己写一个类去实现 Collection 就有可能出错,但通过如上的方式将其全部规范为 Object 类,使用 Object 提供的默认方法就不会出现问题了。

说回刚刚的流程,如果长度为 0 的话,就将 elementData 赋值为 EMPTY_ELEMENTDATA,所以这个 static 属性其实就是标识有参构造方法形成的空 elementData 数组。

如果长度为 0 的话,就将 elementData 赋值为 EMPTY_ELEMENTDATA,所以这个 static 属性其实就是标识有参构造方法形成的空 elementData 数组。


04.底层扩容机制

终于到了重中之重的扩容机制,也是面试题中经常会问到的部分,直接来追一下源代码:

	/**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

add 的代码其实比较简单,首先就是调用了 ensureCapacityInternal() 来保证存储空间(elementData)足够放下这个新的元素,然后再将元素放入其中:elementData[size++] = e;,那接下来要看什么呢?不用多说,肯定是这个 ensureCapacityInternal() 方法。

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

在这个方法中,首先是使用了 calculateCapacity(elementData, minCapacity) 方法去计算容量,这个 minCapacity 是上面传过来的,不管是上面的有参构造还是无参构造,最终形成的 ArrayList 实例的长度都是 0,所以此时的 minCapacity 就是 1。

下面的是 calculateCapacity() 方法

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

首先看第一个 if 语句 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA),先去判断了这个 elementData 是否为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,那什么时候会是这个元素呢?答案就是 无参默认构造方法 创建的实例,这时候就发挥它的作用了,当发现 ArrayList 实例是通过无参构造形成的,就会去取 minCapacity 和 DEFAULT_CAPACITY 中的最小值,而 DEFAULT_CAPACITY 它的值就是 10,这也就是为什么很多面试题的答案说,首先创造空数组,然后第一次扩容的时候扩容成 10;但这并不是完全正确的,当不是无参构造的时候,其实此时的 minCapacity 仍然是 1。

OK,得到了 minCapacity,我们回到上一个方法:

	private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

然后就是取执行 ensureExplicitCapacity() 方法了:

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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

首先是对这个 modCount 做了一个自增,这个变量记录了这个集合扩容的次数,然后去判断 minCapacity - elementData.length > 0 也就是最小需要的长度能否通过当前的 elementData 长度满足,如果不能就进入扩容方法 grow(),并且传入 minCapacity。

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        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);
    }

首先记录下 oldCapacity,也就是扩容之前 elementData 的长度,然后执行这条语句 newCapacity = oldCapacity + (oldCapacity >> 1);,使用了右移运算符,右移运算符其实就可以看作除以 2 的操作,然后再加上原本的 oldCapacity,最终就是原本长度的 1.5 倍,但是因为是最后将其转换为了 int,所以其扩容效果是 小于等于 1.5 倍的;然后后面就是将此时的 minCapacity 和这个由原本长度拓展 1.5 倍的长度做一个对比,取最大,后一个 if 语句是为了处理扩容过限的问题,代码比较容易,最后贴给大家看一下。

最终就是调用 Arrays.copyOf(elementData, newCapacity); 将原本 elementData 中的内容移动到新拓展的,长度为 newCapacity 的数组中,这就完成了一个完整的扩容。

  • 此时如果是无参构造,它带进来的 minCapacity 就是 10,最终其会被拓展为 10
  • 如果是有参构造的话,带进来的 minCapacity 其实就是 1,且计算得 int newCapacity = oldCapacity + (oldCapacity >> 1); 结果是 0,那最终 elementData 会被拓展成 1。

所以说第一次拓展均拓展成 10 其实是不准确的;其他长度的拓展大家顺着流程推导一下就很容易得到了。

最后贴上 hugeCapacity() 方法的源码:

	private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

这个方法是为了处理 newCapacity 的长度超过定义的数组的最大长度(MAX_ARRAY_SIZE,它被定义为 Integer.MAX_VALUE - 8),此时就使用 minCapcaity 进行初始化,如果发现 minCapacity < 0,就大概率是因为越界导致的了,因为当 int 超过 231 - 1 的时候,就会因为错位变成负数,所以此时抛出 OutOfMemoryError 超过内存限制错误,然后判断此时的 minCapacity 是否大于 MAX_ARRAY_SIZE,如果不大于就赋值成它,否则赋值成 Integer.MAX_VALUE,也就是 int 的最大值,如果还是不够会在后面因为越界抛出异常的。

  • 43
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
ArrayList扩容机制源码如下: ```java private void ensureCapacityInternal(int minCapacity) { // 如果当前容量不足,则需要进行扩容操作 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 如果当前容量是默认值,则需要将其扩容为默认容量或者minCapacity minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity); } private void ensureExplicitCapacity(int minCapacity) { modCount++; // 如果需要进行扩容,则进行扩容操作 if (minCapacity - elementData.length > 0) grow(minCapacity); } private void grow(int minCapacity) { // 当前容量 int oldCapacity = elementData.length; // 扩容后的容量 int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果扩容后的容量仍然小于需要的最小容量,则直接使用需要的最小容量 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 如果扩容后的容量超过了ArrayList最大容量,则进行特殊处理 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 创建新的数组,并将原数组中的元素复制到新数组中 elementData = Arrays.copyOf(elementData, newCapacity); } private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; } ``` 在上述代码中,`ensureCapacityInternal`方法首先判断当前容量是否足够,如果不足,则调用`ensureExplicitCapacity`方法进行扩容操作。`ensureExplicitCapacity`方法会比较需要的最小容量和当前容量的差值,如果超过了当前容量,则进行扩容操作。`grow`方法是扩容的核心方法,它会首先计算扩容后的容量,然后根据扩容后的容量创建新的数组,并将原数组中的元素复制到新数组中。如果扩容后的容量超过了ArrayList最大容量,则进行特殊处理。`hugeCapacity`方法用于计算需要的最大容量,如果超过了最大容量,则抛出OutOfMemoryError异常。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

*Soo_Young*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值