ArrayList源码分析

5 阅读6分钟

ArrayList的核心字段

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;

通过上方可以看出默认的容量是10,还存在两个空数组,以及底层的真正的数据数组与实际的数据大小。

  • 底层是数组:所有元素连续存储在内存中

  • size ≠ capacity:size是元素数量,capacity是数组长度

  • 自动扩容:当数组满时自动创建更大的新数组

  • EMPTY_ELEMENTDATA:表示一个真正的空数组实例,当ArrayList通过指定初始容量为0的构造函数创建时使用。在添加第一个元素时,会直接分配一个所需大小的新数组(最小为1),不涉及默认容量逻辑。

  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:作为一个特殊标记,表示使用默认初始容量(通常为10)。当ArrayList通过默认构造函数(无参数)创建时使用。在添加第一个元素时,会触发延迟内存分配,直接分配一个大小为默认容量(10)的新数组。

底层数组的序列化

我们发现真实的底层数组使用了transient进行修饰,表明这个字段不会被Java默认的序列化机制处理,因为 elementData数组的容量(length)通常大于实际存储的元素个数(size)。如果直接序列化整个数组,会序列化大量无用的 null值,浪费空间和时间。

ArrayList自己实现了writeObjecreadObject方法。在这两个方法中,它手动地、只序列化和反序列化实际包含的元素(从 0到 size-1),而不是整个elementData数组。 源码如下:

    @java.io.Serial
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioral compatibility with clone()
        s.writeInt(size);

        // Write out all elements in the proper order.
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }
    @java.io.Serial
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored

        if (size > 0) {
            // like clone(), allocate array based upon size not capacity
            SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size);
            Object[] elements = new Object[size];

            // Read in all elements in the proper order.
            for (int i = 0; i < size; i++) {
                elements[i] = s.readObject();
            }

            elementData = elements;
        } else if (size == 0) {
            elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new java.io.InvalidObjectException("Invalid size: " + size);
        }
    }

ArrayList的构造方法

ArrayList有三种构造方法:

  1. 无参构造
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

无参构造是直接将DEFAULTCAPACITY_EMPTY_ELEMENTDATA这个空数组赋给底层数组。

  1. 有参构造
    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);
        }
    }

有参构造中传入一个初始的大小,如果这个值大于0,则创建一个大小为这个值的数组,如果等于0,则将一个空数组赋给底层数组。

  1. 集合参数构造
    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就不需要拷贝了呢?

    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

原来ArrayList中的toArray方法已经是拷贝底层数组了。

ArrayList的增加元素

    public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }

它实际上会调用一个内部方法add

    private void add(E e, Object[] elementData, int s) {
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }

如果当前的底层数组的长度与当前ArrayList的大小相等,意味着需要扩容,之后再加入数据。

扩容机制1

    private Object[] grow() {
        return grow(size + 1);
    }

grow方法会调用另一个grow方法,并传入一个minCapacity,这代表了至少需要的容量

    private Object[] grow(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                    minCapacity - oldCapacity, /* minimum growth */
                    oldCapacity >> 1           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        } else {
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }
    public static int newLength(int oldLength, int minGrowth, int prefGrowth) {

        int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
        if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
            return prefLength;
        } else {
            // put code cold in a separate method
            return hugeLength(oldLength, minGrowth);
        }
    }

当底层数组有数据时,会调用ArraysSupport.newLength,其逻辑是旧数组的长度+ minCapacity - oldCapacity oldCapacity >> 1的最大值。

  • 当要求新增的容量<1.5倍原容量时,数组扩容为原先的1.5倍
  • 当要求新增的容量>1.5倍原容量时,数组扩容为旧容量+要求新增的容量

当底层数组是EMPTY_ELEMENTDATA时,依旧调用ArraysSupport.newLength,此时oldCapacity>>1=0,故此时数组扩容为要求新增的容量。

当底层数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,数组扩容为DEFAULT_CAPACITY和要求新增容量的最大值。

为什么总是需要和要求新增的容量对比呢?原因很简单,因为扩容之后起码要容纳的下所有的数据。

扩容机制2

newLength方法中如果即将返回的数组大小超过了SOFT_MAX_ARRAY_LENGTH或者小于0,将使用hugeLength,传入oldGrowthminGrowth

    private static int hugeLength(int oldLength, int minGrowth) {
        int minLength = oldLength + minGrowth;
        if (minLength < 0) { // overflow
            throw new OutOfMemoryError(
                "Required array length " + oldLength + " + " + minGrowth + " is too large");
        } else if (minLength <= SOFT_MAX_ARRAY_LENGTH) {
            return SOFT_MAX_ARRAY_LENGTH;
        } else {
            return minLength;
        }
    }

当小于0时,说明两者相加溢出了,会报错,当minLength <= SOFT_MAX_ARRAY_LENGTH时,说明在newLengtholdLength+prefGrowth超过了这个界限,因此尽可能分配更大的空间。

如果minLength> SOFT_MAX_ARRAY_LENGTH,则直接返回minLength

SOFT_MAX_ARRAY_LENGTH的作用是什么?

为底层数组的扩容提供一个比理论最大值(Integer.MAX_VALUE)更小、更安全的容量上限。其主要目的是确保在极端扩容场景下,为 JVM 的对象头预留空间,提高内存分配的可靠性和跨 JVM 实现的兼容性,防止因数组过大而直接引发底层内存错误。

添加大容量数据的场景

因为ArrayList是动态扩容的,如果需要增加很多数据,不应该一个个加入,这样可能会触发多次扩容,导致效率极低。

应该在加入元素前,使用ensureCapacity来判断是否需要扩容,并直接扩容到对应的容量。

    public void ensureCapacity(int minCapacity) {
        if (minCapacity > elementData.length
            && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
                 && minCapacity <= DEFAULT_CAPACITY)) {
            modCount++;
            grow(minCapacity);
        }
    }

RandomAccess

ArrayList实现了RandomAccess接口,这个接口的存在主要是为了给泛型算法提供性能优化的提示。

例如,Collections工具类中的一些方法(如 Collections.binarySearch, Collections.shuffle, Collections.reverse等)会根据传入的 List是否实现了 RandomAccess选择不同的底层算法,以达到最佳性能。

  • 对于实现了 RandomAccessList(如 ArrayList :可能会使用基于索引的循环遍历,效率更高。
  • 对于未实现 RandomAccessList(如 LinkedList :可能会使用迭代器进行顺序访问,避免因频繁的 get(index)调用导致的性能灾难。

Vector

  • ArrayListList 的主要实现类,底层使用 Object[]存储,适用于频繁的查找工作,线程不安全 。
  • VectorList 的古老实现类,底层使用Object[] 存储,线程安全。

Vector之所以线程安全,是因为其方法上使用了synchronized修饰