3.jdk源码阅读之ArrayList(上)

1.写在前面

我个人认为看代码不应该流水账一般的看,而应该带着问题去看。换个说法就是你想通过阅读源码解答你心中什么疑问?
回到jdk源码,ArrayList应该是java程序员日常开发中用的比较多的类。
有以下几个问题,我觉得可以从源码中找到答案:
1.当向ArrayList中添加元素的时候,如果容量不够了,它是怎么扩容的?什么时候扩容?扩到多大?
2.我们都知道ArrayList线程不安全,那是什么导致的线程不安全呢?
3.ArrayList底层是通过什么数据结构来实现的?
下面我们就带着这些问题,来研究下jdk8 的ArrayList源码。

2.纵观全局

ArrayList继承树结构
上图是ArrayList的继承树结构,通过该图可以看出以下几点:

  1. ArrayList 继承于 AbstractList,实现了 Cloneable、List、RandomAccess、Serializable 这些接口。
  2. ArrayList 继承了 AbstractList,实现了 List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
  3. ArrayList 实现了 Cloneable 接口,即覆盖了函数 clone(),所以它能被克隆。
  4. ArrayList 实现了 RandmoAccess 接口,因此提供了随机访问功能。
  5. ArrayList 实现了 Serializable 接口,这意味着 ArrayList 支持序列化。

3.从使用到原理

一般我们使用ArrayList的场景基本和下面的代码类似:

    List<String> list = new ArrayList<>();
        list.add("a");
        list.add("b");
        list.add("c");

3.1空构造器的底层实现

我们来看看调用ArrayList的时候底层做了什么

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

我们发现是给elementData这个变量赋值了一个常量值 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
从官方注释我们可以知道这里是在初始化一个初始容量为10的空List。
我们来看看elementData和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 = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     */
    transient Object[] elementData; // non-private to simplify nested class access

3.1.1 DEFAULTCAPACITY_EMPTY_ELEMENTDATA

官方注释的翻译:共享的空数组实例用于默认大小的空实例。我们将它与 EMPTY_ELEMENTDATA 区分开来,以便在添加第一个元素时知道需要扩容多少。
注释中提到了另外一个变量 EMPTY_ELEMENTDATA

    /**
     * Shared empty array instance used for empty instances.
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

就目前来看 EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA 除了名字,其他的没什么区别。
那这两个数组各有不同的作用呢?这里先告诉大家答案,然后我们再从代码中去验证。
在 ArrayList 的实现中,存在两个不同的空数组实例:

  • 默认大小的空数组实例(DEFAULTCAPACITY_EMPTY_ELEMENTDATA):这个空数组用于初始化那些没有指定初始容量的 ArrayList 实例。它表示一个默认大小的空 ArrayList。
  • EMPTY_ELEMENTDATA:这是另一个表示空数组的实例。它用于那些在初始化时明确指定了容量为 0 的 ArrayList 实例。

这两个空数组实例的区分很重要,因为它们在 ArrayList 第一次添加元素时的行为是不同的:

  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:当第一次添加元素时,ArrayList 需要根据默认的初始容量进行扩容。
  • EMPTY_ELEMENTDATA:当第一次添加元素时,ArrayList 需要根据指定的初始容量进行扩容。

通过区分这两个空数组实例,ArrayList 可以在第一次添加元素时正确地进行扩容操作,确保性能和内存使用的优化。

3.1.2 elementData

官方注释的翻译:ArrayList 的元素存储在一个数组缓冲区中。ArrayList 的容量是这个数组缓冲区的长度。任何 elementData 等于 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 的空 ArrayList,在添加第一个元素时,其容量将扩展到 DEFAULT_CAPACITY。
注释中提到的DEFAULT_CAPACITY 在源代码中的定义如下:

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

这里就涉及到一个面试题:
ArrayList 默认初始容量是多少?答案显而易见了是10。
先来解释下官方注释中几个概念,然后再在源码中验证:

  1. 数组缓冲区:ArrayList 的元素存储在一个内部的数组中,这个数组被称为数组缓冲区。
  2. 容量:ArrayList 的容量是指这个数组缓冲区的长度,而不是当前元素的数量。容量决定了 ArrayList 在需要扩容之前可以容纳的最大元素数量。
  3. 默认容量扩展:
    • 当一个空的 ArrayList 的 elementData 等于 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,这意味着该 ArrayList 是使用默认构造函数创建的,初始状态是一个空数组。
    • 当向这样的 ArrayList 添加第一个元素时,内部的数组缓冲区将扩展到默认容量 DEFAULT_CAPACITY,以便容纳新元素。

3.2 向ArrayList中添加元素的底层原理

我们来看看调用add()方法后底层做了什么,源码如下:

  /**
     * 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;
    }

可以看到,只有3行代码,我们分别来看下。
先来看官方注释翻译:将指定的元素追加到此列表的末尾

3.2.1 ensureCapacityInternal(size + 1):

  • 这个方法用于确保内部数组 elementData 有足够的容量来存储新添加的元素。
  • 如果内部数组的容量不足以容纳新元素,该方法会扩展数组的容量。
  • size + 1 表示当前元素数量加上即将添加的一个新元素。
    ensureCapacityInternal的源代码如下:
 private void ensureCapacityInternal(int minCapacity) {
        if (elementData == EMPTY_ELEMENTDATA) {
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);
    }

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

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

确保内部数组 elementData 的容量至少为 minCapacity。如果当前数组为空(即 EMPTY_ELEMENTDATA),则将最小容量设置为默认容量和 minCapacity 中的较大值。

  1. 检查是否为空数组
if (elementData == EMPTY_ELEMENTDATA) {
    minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
  • elementData 是 ArrayList 内部存储元素的数组。
  • EMPTY_ELEMENTDATA 是一个空数组的常量。
  • 如果当前数组是空数组,则将 minCapacity 设置为默认容量 DEFAULT_CAPACITY 和 minCapacity 中的较大值,以确保数组有一个合理的初始容量。
3.2.1.2 ensureExplicitCapacity

显式地确保内部数组的容量至少为 minCapacity。如果当前容量不足以容纳 minCapacity,则调用 grow 方法来扩展数组。

  1. 增加修改计数
modCount++;

modCount 是一个用于记录 ArrayList 结构修改次数的变量。每次添加或删除元素时,modCount 会增加,用于支持快速失败机制(fail-fast)。

  1. 检查容量并扩展
if (minCapacity - elementData.length > 0)
    grow(minCapacity);
  • 如果 minCapacity 大于当前数组的长度,则调用 grow 方法来扩展数组。
  • grow 方法会根据需要扩展数组的容量,以确保能够容纳新的元素。

3.2.2 elementData[size++] = e

  • 将新元素 e 添加到内部数组 elementData 的位置 size。
  • size 是当前 ArrayList 中元素的数量。
  • size++ 是一个后缀递增操作,先使用 size 的当前值,然后将 size 增加 1。

3.2.3 grow

我们来看下扩容方法grow,源代码如下:

  /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    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);
    }

这个方法的作用是增加内部数组的容量,以确保其至少能够容纳 minCapacity 个元素。我们拆解来看下。

  1. 计算旧容量
int oldCapacity = elementData.length;
  1. 计算新容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
  • newCapacity 是新的容量,计算方法是旧容量加上旧容量的一半。这种扩展策略可以避免频繁地扩展数组,提高性能
  • oldCapacity >> 1 是右移操作,相当于 oldCapacity / 2。
    这里就会涉及到一个面试题:ArrayList每次扩容,扩多少?
  1. 检查新容量是否小于最小容量
if (newCapacity - minCapacity < 0)
    newCapacity = minCapacity;
  • 如果计算出的新容量 newCapacity 小于所需的最小容量 minCapacity,则将新容量设置为 minCapacity。
  1. 检查新容量是否超过最大数组大小
if (newCapacity - MAX_ARRAY_SIZE > 0)
    newCapacity = hugeCapacity(minCapacity);

  • MAX_ARRAY_SIZE 是允许的最大数组大小。如果新容量超过这个值,则调用 hugeCapacity 方法来处理非常大的容量需求。
  • hugeCapacity 方法会进一步检查并调整容量,以避免内存溢出。这个方法我们一会再看。
  1. 扩展数组
  • 使用 Arrays.copyOf 方法创建一个新数组,并将旧数组中的元素复制到新数组中。
  • 新数组的容量为 newCapacity。

3.2.4 hugeCapacity

hugeCapacity 方法是 ArrayList 内部用于处理非常大容量需求的辅助方法。当需要扩展 ArrayList 的容量时,如果计算出的新容量超过了预定义的最大数组大小 MAX_ARRAY_SIZE,则会调用这个方法来确定最终的容量。源代码如下:

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

根据给定的最小容量 minCapacity,确定一个合理的最大容量。如果 minCapacity 超过了允许的最大数组大小 MAX_ARRAY_SIZE,则返回 Integer.MAX_VALUE;否则,返回 MAX_ARRAY_SIZE。

  1. 检查溢出
if (minCapacity < 0) // overflow
    throw new OutOfMemoryError();

  • 如果 minCapacity 小于 0,说明发生了整数溢出。这种情况下,抛出 OutOfMemoryError 异常,因为不可能分配负数大小的数组。
  1. 确定返回值
return (minCapacity > MAX_ARRAY_SIZE) ?
    Integer.MAX_VALUE :
    MAX_ARRAY_SIZE;

  • 如果 minCapacity 大于 MAX_ARRAY_SIZE,则返回 Integer.MAX_VALUE。这表示允许的最大数组大小。
  • 如果 minCapacity 小于或等于 MAX_ARRAY_SIZE,则返回 MAX_ARRAY_SIZE。
    MAX_ARRAY_SIZE的定义如下:
  /**
     * The maximum size of array to allocate.
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

有的网友看到这可能会问为什么MAX_ARRAY_SIZE要比Integer.MAX_VALUE小8而不是7?不是6?不是16?
MAX_ARRAY_SIZE 被定义为 Integer.MAX_VALUE - 8 是为了处理 JVM 在管理数组对象时的一些内部开销。具体来说,JVM 需要一些额外的空间来存储数组的元数据(metadata),例如数组的长度、类型信息等。这些开销使得实际可用的数组大小略小于 Integer.MAX_VALUE。

  • JVM 内部开销
    JVM 在分配数组对象时,不仅仅需要存储数组的元素,还需要存储一些元数据。这些元数据包括数组的长度、数组的类型等。
    因此,实际可用的数组大小必须比 Integer.MAX_VALUE 小一些,以确保 JVM 有足够的空间来存储这些元数据。
  • 避免溢出
    如果 ArrayList 的容量扩展到接近 Integer.MAX_VALUE,在某些情况下可能会导致整数溢出。例如,当计算新容量时,可能会发生 oldCapacity + (oldCapacity >> 1) 超过 Integer.MAX_VALUE 的情况。定义 MAX_ARRAY_SIZE 为 Integer.MAX_VALUE - 8 可以避免这种溢出情况。
  • 安全性和稳定性
    将 MAX_ARRAY_SIZE 设置为 Integer.MAX_VALUE - 8 是一种保守的做法,确保在极端情况下,ArrayList 仍能稳定运行。这种做法可以防止在分配非常大数组时可能出现的 OutOfMemoryError 或其他异常情况。

由于篇幅原因,剩下的关于ArrayList的源码解读我们放到下一篇文章中。

系列文章

1.JDK源码阅读之环境搭建

2.JDK源码阅读之目录介绍

3.jdk源码阅读之ArrayList(上)

4.jdk源码阅读之ArrayList(下)

5.jdk源码阅读之HashMap

6.jdk源码阅读之HashMap(下)

7.jdk源码阅读之ConcurrentHashMap(上)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

至真源

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

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

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

打赏作者

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

抵扣说明:

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

余额充值