ArrayList工作原理及底层源码实现

一.概述

以数组实现。节约空间,但数组有容量限制。超出限制时会增加50%容量,用System.arraycopy()复制到新的数组。因此最好能给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。

按数组下标访问元素-get(i)、set(i,e) 的性能很高,这是数组的基本优势。

如果按下标插入元素、删除元素-add(i,e)、 remove(i)、remove(e),则要用System.arraycopy()来复制移动部分受影响的元素,性能就变差了。

越是前面的元素,修改时要移动的元素越多。直接在数组末尾加入元素-常用的add(e),删除最后一个元素则无影响。

ArrayList的源码注释是这样说的:

* Resizable-array implementation of the <tt>List</tt> interface.  Implements
 * all optional list operations, and permits all elements, including
 * <tt>null</tt>.  In addition to implementing the <tt>List</tt> interface,
 * this class provides methods to manipulate the size of the array that is
 * used internally to store the list.  (This class is roughly equivalent to
 * <tt>Vector</tt>, except that it is unsynchronized.)

大致的意思是:
是List 接口的大小可变数组的实现,实现所有可选的列表操作,并允许所有元素,包括null。除了实现List接口之外,这个类还提供了一些方法来操纵数组的大小,以便在内部储存列表。(这个类大致相当于矢量,只是它是不同步的。)

二.构造方法

下面是从jdk1.8直接复制下来的ArrayList类的源码,我加了一些中文翻译,和简单的注释方便阅读

    //默认初始化容量
    private static final int DEFAULT_CAPACITY = 10;
 
    //空对象数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
 
    //对象数组
    private transient Object[] elementData;
 
    //集合元素个数
    private int size;

     /**
     * Constructs an empty list with the specified initial capacity.
     * 用指定的初始容量构造一个空列表
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    public ArrayList(int initialCapacity) {
        //传入的容量大于0的情况,创建一个指定长度的空数组
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        //传入容量为0的情况,创建一个长度为0的空数组
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * Constructs an empty list with an initial capacity of ten.
     * 构造一个空列表,初始容量为10。
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     * 构造一个包含指定元素的列表的列表list,按照集合的顺序返回迭代器。
     * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    public ArrayList(Collection<? extends E> c) {
        // 持有传入集合的内部数组的引用
        elementData = c.toArray();
        // 如果传入的集合长度不为0
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            // 判断引用的数组类型, 并将引用转换成Object数组引用
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            // 如果传入的集合为0,创建一个空数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

可以看到ArrayList的内部存储结构就是一个Object类型的数组,因此它可以存放任意类型的元素。在构造ArrayList的时候,如果传入初始大小那么它将新建一个指定容量的Object数组,如果不设置初始大小那么它将不会分配内存空间而是使用空的对象数组,在实际要放入元素时再进行内存分配。下面再看看它的增删改查方法。

三.add方法

/**
     * Appends the specified element to the end of this list.
     * 将指定的元素附加到列表的末尾。
     */
    public boolean add(E e) {
        // 添加前先检查是否需要拓展数组, 此时数组长度最小为size+1
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 将元素添加到集合的末尾
        elementData[size++] = e;
        return true;
    }

    /**
     * Inserts the specified element at the specified position in this
     * list. Shifts the element currently at that position (if any) and
     * any subsequent elements to the right (adds one to their indices).
     * 在指定的位置插入指定的元素。改变当前位置的元素(如果有的话),并移动后边的所有
     * 后续元素(增加一个到它们的索引)。
     */
    public void add(int index, E element) {
        // 检查索引是否在指定返回内(0到size之间)
        rangeCheckForAdd(index);
        // 添加前先检查是否需要拓展数组, 此时数组长度最小为size+1
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 移动后面元素的位置
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }

我们可以看到:
1.add(e)方法直接在数组的最后添加元素,操作较快,
2.add(int,e)方法是在指定位置插入元素,需要移动后面的元素,速度较慢,
3.他们的实现其实最核心的内容就是ensureCapacityInternal。这个函数其实就是自动扩容机制的核心。我们依次来看一下他的具体实现

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

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

        // overflow-conscious code
        // 如果最小容量大于数组长度就扩增数组
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
 /**
     * 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;

        // 扩展为原来的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);

        // 如果扩展为1.5倍后还不满足需求,则直接扩展为需求值
        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);
    }

我们可以看到:
1.ensureCapacityInternal方法直接调用ensureExplicitCapacity方法实现这种最低要求的存储能力,并传入了实际存储存元素的数组elementData,和需要的最小容量mincapacity,
2.ensureExplicitCapacity方法中,判断需要最小的容量大于数组本身容量,就进行扩容调用grow方法,
3.grow中先获取到数组原有的容量,并扩展到原来的1.5倍,再校验是否满足,如果不满足就直接扩展的需要的最小容量,最后将原来的数组拷贝到新的数组.

四.remove方法

 /**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).
     * 删除该列表中指定位置的元素,将所有后续元素转移到前边(将其中一个元素从它们中减去指数)
     */
    public E remove(int index) {
        // 检查传入的下标是否合理
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 将index后面的元素向前挪动一位
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // GC
        elementData[--size] = null; // clear to let GC do its work
    
        return oldValue;
    }

remove方法,由于需要将删除位置后面的元素向前挪动,也会设计数组复制,所以操作较慢。

五.set/get方法

 /**
     * Returns the element at the specified position in this list.
     * 返回列表中指定位置的元素
     */
    public E get(int index) {
        // index不能大于size
        rangeCheck(index);
        
        // 返回指定位置元素
        return elementData(index);
    }
/**
     * Replaces the element at the specified position in this list with
     * the specified element.
     * 在这个list中替换指定位置的元素为指定的元素。
     */
    public E set(int index, E element) {
        // index不能大于size
        rangeCheck(index);
        
        // 返回指定位置元素
        E oldValue = elementData(index);
        //替换成新元素
        elementData[index] = element;
        return oldValue;
    }

set:直接对指定位置元素进行修改,不涉及元素挪动和数组复制,操作快速。

get:直接返回指定下标的数组元素,操作快速。

六.小结

从以上源码可以看出

1. ArrayList底层实现是基于数组的,因此对指定下标的查找和修改比较快,但是删除和插入操作比较慢。

2. 构造ArrayList时尽量指定容量,减少扩容时带来的数组复制操作,如果不知道大小可以赋值为默认容量10。

3. 每次添加元素之前会检查是否需要扩容,扩容是增加原有容量的一半,如果扩容之后还不满足将直接增加到所需要的容量。

4. 每次对下标的操作都会进行安全性检查,如果出现数组越界就立即抛出异常。

5. ArrayList的所有方法都没有进行同步,因此它不是线程安全的。

6. 以上分析基于JDK1.8,其他版本会有些出入,因此不能一概而论。

参考资料:

https://mp.weixin.qq.com/s/lln6qnfIXffqPwXvRZBTcQ

https://www.javazhiyin.com/181.html

猜你喜欢

转载自blog.csdn.net/iteen/article/details/82194871