JDK8和JDK17的ArrayList 源码对比分析

目录

    属性

构造方法

ArrayList的扩容机制

jdk8的扩容机制

jdk17的扩容机制

ArrayList的JDK 8和JDK 17扩容机制的对比


    属性

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
   // 序列化版本号
    @java.io.Serial
    private static final long serialVersionUID = 8683452581122892189L;

   // 默认初始容量
    private static final int DEFAULT_CAPACITY = 10;

    // 空数组,用于空实例
    private static final Object[] EMPTY_ELEMENTDATA = {};

   // 默认空数组,用于默认大小的空实例
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

   // 存储ArrayList元素的数组
    transient Object[] elementData; // non-private to simplify nested class access

   // ArrayList的大小(包含的元素个数)
    private int size;
}

ArrayList源码中定义的默认容量DEFAULT_CAPACITY等于10,定义了两个空数组,以及一个真正存储元素的Object数组。

构造方法

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

空参构造方法会将默认数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋值给真正存储对象的数组elementData,在后续添加元素的时候再使用到默认容量,将数组大小扩容到10。

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);
    }
}

传入需要构建的ArrayList的初始大小,传入的initialCapacity如果等于0,则将属性中的空数组EMPTY_ELEMENTDATA赋值给真正存储对象的数组elementData。

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

构造包含指定集合元素的列表

ArrayList的扩容机制

jdk8的扩容机制

// 添加元素到末尾
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    elementData[size++] = e;
    return true;
}

// 在指定位置插入元素
public void add(int index, E element) {
    rangeCheckForAdd(index);
    ensureCapacityInternal(size + 1);
    // 将index及其之后的元素后移一位
    System.arraycopy(elementData, index, elementData, index + 1, size - index);
    elementData[index] = element;
    size++;
}

       在判断了扩容之后,就用System.arraycopy()进行原地数组的复制,将index开始的元素往后移一个位置,最后给数组的index的位置赋值。

       可能有些读者没见过System.arraycopy()这个方法,此方法是一个本地方法,我们常用的Arrays.copyOf()方法底层其实就是调用了这个方法,如下图中。

 既然Arrays.copyOf()本质上调用的是System.arraycopy()方法,那么效率上Arrays.copyOf()肯定能是不及System.arraycopy()的。在这里粘一下源码,大家可以阅读一下。

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

接下来讲解一下jdk8中的扩容机制

ensureCapacityInternal(int minCapacity) 方法

private void ensureCapacityInternal(int minCapacity) {
    // 检查当前数组是否是默认空数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 如果是,确保最小容量至少为默认容量
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    // 确保显式容量
    ensureExplicitCapacity(minCapacity);
}

       确保 ArrayList 的容量足够以容纳至少 minCapacity 个元素,minCapacity 表示需要的最小容量。

      如果当前数组是默认空数组(即未初始化),则将 minCapacity 设置为默认容量(10)和传入的 minCapacity 中的较大值,调用 ensureExplicitCapacity 方法来检查并扩容。

private void ensureExplicitCapacity(int minCapacity) {
    modCount++; // 增加修改计数,确保在迭代时可以检测到并发修改
    // 检查当前容量是否小于所需的最小容量
    if (minCapacity - elementData.length > 0)
        grow(minCapacity); // 如果小于,调用 grow 方法进行扩容
}

       增加 modCount,用于检测并发修改。如果当前数组的长度小于 minCapacity,则调用 grow 方法进行扩容。

 grow 方法

private void grow(int minCapacity) {
    int oldCapacity = elementData.length; // 获取当前数组的容量
    // 计算新的容量,通常为旧容量的1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    // 如果新的容量小于所需的最小容量,则使用最小容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果新的容量超过最大数组大小,则调用 hugeCapacity 方法处理
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 扩容并返回新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

 获取当前数组的容量 oldCapacity,计算新的容量 newCapacity,通常为旧容量的1.5倍(oldCapacity + (oldCapacity >> 1))。如果 newCapacity 小于 minCapacity,则将 newCapacity 设置为 minCapacity,如果 newCapacity 超过最大数组大小(MAX_ARRAY_SIZE),则调用 hugeCapacity 方法处理,最后使用 Arrays.copyOf 方法扩容并返回新数组。

hugeCapacity(int minCapacity) 方法

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // 检查是否溢出
        throw new OutOfMemoryError(); // 如果溢出,抛出内存不足错误
    // 返回最大数组大小或 Integer.MAX_VALUE
    return (minCapacity > MAX_ARRAY_SIZE) ?
        Integer.MAX_VALUE : // 如果需要的容量超过最大数组大小,返回 Integer.MAX_VALUE
        MAX_ARRAY_SIZE; // 否则返回最大数组大小
}

 检查 minCapacity 是否小于0(即是否发生溢出),如果是,则抛出 OutOfMemoryError,如果 minCapacity 超过 MAX_ARRAY_SIZE,则返回 Integer.MAX_VALUE,表示无法创建更大的数组,否则,返回 MAX_ARRAY_SIZE。

jdk17的扩容机制

在jdk17中有三个add方法,先介绍其中一个

//公有方法
public void add(int index, E element) {
      //对传入的index进行判断,若是非法的数据则抛出异常
        rangeCheckForAdd(index);
     //用于fail-fast(快速失败)机制
        modCount++;
        final int s;
        Object[] elementData;
     //
        if ((s = size) == (elementData = this.elementData).length)
            elementData = grow();
    //进行数组的复制
        System.arraycopy(elementData, index,
                         elementData, index + 1,
                         s - index);
        elementData[index] = element;
        size = s + 1;
    }
private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

       add(int index, E element)该方法会在指定的数组位置上添加元素。如代码注释如上,modCount值是用于快速失败机制的,在后文会进行进一步讲解。在代码中会拿ArrayList中的size和元素数组elementData的大小进行比较,若是相等则说明现在已经到边界,接下来就需要调用grow()方法进行扩容。

       在判断了扩容之后,就用System.arraycopy()进行原地数组的复制,将index开始的元素往后移一个位置,最后给数组的index的位置赋值。

       可能有些读者没见过System.arraycopy()这个方法,此方法是一个本地方法,我们常用的Arrays.copyOf()方法底层其实就是调用了这个方法,如下图中。

 既然Arrays.copyOf()本质上调用的是System.arraycopy()方法,那么效率上Arrays.copyOf()肯定能是不及System.arraycopy()的。在这里粘一下源码,大家可以阅读以下

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

还有两个add方法是相互调用的,代码如下

//公有方法
public boolean add(E e) {
     //用于fail-fast(快速失败)机制
        modCount++;
     //执行重载add方法,传入元素,elementData数组和size
        add(e, elementData, size);
        return true;
    }

//私有方法
private void add(E e, Object[] elementData, int s) {
     //判断数组是否需要扩容,若是需要扩容,就执行grow()方法
        if (s == elementData.length)
            elementData = grow();
     //给传入的s位置赋值元素
        elementData[s] = e;
    //将元素的size改为s+1
        size = s + 1;
    }

      代码注释如上,就是调用了重载的方法,判断是否需要扩容,最后在进行赋值,也不过多叙述了。

接下来就来讲解一下扩容的过程

 private Object[] grow() {
    return grow(size + 1); // 调用 grow 方法,传入当前大小 + 1,表示至少需要一个额外的空间
}
 private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length; // 获取当前数组的容量
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        // 如果当前容量大于0,或者不是默认空数组
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        // 计算新的容量,使用 ArraysSupport.newLength 方法
        return elementData = Arrays.copyOf(elementData, newCapacity); // 扩容并返回新数组
    } else {
        // 如果是默认空数组,初始化为默认容量
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

       代码的详细注释如上图,扩容的过程会调用ArraysSupport类的方法,记住这里分别传入的值为原elementData数组的大小,最小需要的扩容数,还有原elementData数组的大小左移一位,约为elementData数组的大小的0.5倍,elementData数组的大小是偶数就是0.5倍,否则会不足0.5倍,有精度丢失。

      接下来讲解调用的ArraysSupport类的方法

//oldLength:当前数组的长度。
//minGrowth:最小需要增加的长度。
//prefGrowth:首选增长长度(通常是当前长度的一半)
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
    // preconditions not checked because of inlining
    // assert oldLength >= 0
    // assert minGrowth > 0

    int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // 计算首选的新长度
    if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
        return prefLength; // 如果在合理范围内,返回首选长度
    } else {
        // 如果超出范围,调用 hugeLength 方法处理
        return hugeLength(oldLength, minGrowth);
    }
}
private static int hugeLength(int oldLength, int minGrowth) {
    int minLength = oldLength + minGrowth; // 计算最小需要的长度
    if (minLength < 0) { // 检查是否溢出
        throw new OutOfMemoryError(
            "Required array length " + oldLength + " + " + minGrowth + " is too large");
    } else if (minLength <= SOFT_MAX_ARRAY_LENGTH) {
        return SOFT_MAX_ARRAY_LENGTH; // 如果在合理范围内,返回 SOFT_MAX_ARRAY_LENGTH
    } else {
        return minLength; // 否则返回计算的最小长度
    }
}

        代码的注解如上,需要说明的是,扩容的大小会取Math.max(minGrowth, prefGrowth)两者的最大值,以便扩容后的容量可以容纳添加的元素数量,一般add方法只添加一个元素,minGrowth=1,所以会扩容到1.5倍左右的大小,但是如果是addAll方法添加一系列元素,且添加元素后需要的大小大于1.5倍,那么就会扩容至刚好能容纳所添加元素的大小。

       这样做不会造成频繁扩容,可以一次添加大量元素,也只会扩容一次。

       JDK 17 的 ArrayList 扩容机制通过 grow 方法和 newLength 方法的组合,灵活地处理了不同情况下的扩容需求。

  • 性能优化:通过计算首选长度和最小增长,避免了不必要的扩容,提升了性能。
  • 安全性:通过溢出检查和合理范围限制,确保了内存的安全使用。
  • 可维护性:将扩容逻辑分离到 ArraysSupport 类中,使得代码更清晰,易于维护。

这种设计使得 ArrayList 在处理动态数组时更加高效和安全,能够适应不同的使用场景。

ArrayList的JDK 8和JDK 17扩容机制的对比

       JDK 8 和 JDK 17 的扩容机制并不是绝对的1.5倍扩容,两者都允许根据实际需要动态调整容量。JDK 17 的实现通过更灵活的扩容策略和更清晰的代码结构,提升了性能和可维护性,JDK 8扩容逻辑较为简单,分为几个步骤,JDK 17的逻辑更为集中,减少了代码的复杂性。

       个人去测试过JDK 8 和 JDK 17 中ArrayList添加数据的一个表现情况,JDK17中ArrayList的效率大概会比JDK 8有5%左右的提升。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值