当前位置 博文首页 > 轩辕慕雨:ArrayList源码分析(JDK1.8)

    轩辕慕雨:ArrayList源码分析(JDK1.8)

    作者:轩辕慕雨 时间:2021-02-03 16:24

    概述

    ArrayList底层是基于数组实现的,并且支持动态扩容的动态数组(变长的集合类)。ArrayList允许空值和重复的元素,当向ArrayList中添加元素数量大于其底层数组容量时,会通过扩容机制重新生成一个容量更大的数组。另外,由于ArrayList底层数据结构是数组,所以保证了在O(1)复杂度下完成随机查找操作。ArrayList是非线程安全的,在并发环境下,多个线程同时操作ArrayList会引发不可预知的错误。

    从上面的类图可以看出,ArrayList实现了4个接口和继承了1个抽象类,分别是:

    • List接口:主要提供了数组的添加、删除、修改、迭代遍历等操作;
    • Cloneable接口:标识克隆操作;
    • Serializable接口:标识可序列列化操作;
    • RandomAccess接口:标识可随机访问操作;
    • 继承AbstractList抽象类,主要提供迭代遍历相关操作。

    属性

    ArrayList的属性比较少,只有两个属性:elementData和size。

    1 private static final int DEFAULT_CAPACITY = 10;
    2 
    3 private static final Object[] EMPTY_ELEMENTDATA = {};
    4 
    5 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    6 // 底层存放元素的数组
    7 transient Object[] elementData;
    8 // elementData数组中元素的数量 
    9 private int size;

    构造方法

    ArrayList有3个构造方法,分别如下:

    • ArrayList(int initialCapcity):根据传入的initialCapacity初始化容量来创建elementData数组;
    • ArrayList():无参构造方法;
    • ArrayList(Collection<? extends E> c):使用传入的集合C作为ArrayList的elementData;
    // 1. ArrayList(int initialCapacity)构造方法
    // 需要注意:当初始化容量initialCapacity为0时,使用EMPTY_ELEMENTDATA对象创建一个空数组,在添加元素的时候,会进行扩容创建需要的数组 public ArrayList(int initialCapacity) { if (initialCapacity > 0) {
           // 初始化容量大于0,创建Object数组
    this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) {
           // 初始化容量为0时,使用EMPTY_ELEMENTDATA对象
    this.elementData = EMPTY_ELEMENTDATA; } else {
           // 参数异常
    throw new IllegalArgumentException("Illegal Capacity:" + initialCapacity); } } // 2. ArrayList()无参构造方法 public ArrayList() {
         // 实际创建的时候是空数组,在首次添加元素的时候,才会初始化容量为10的数组
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } // 3. ArrayList(Collection<? extends E> c)构造方法 public ArrayList(Collection<? extends E> c) {
         // elementData指向c.toArray() elementData
    = c.toArray(); if ((size = elementData.length) != 0) {
           // 如果集合元素的类型不是Object.class类型,则拷贝成Object.class类型
    // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } }

    插入元素

    对于数组(线性表)结构,插入操作分为两种情况:① 在元素序列尾部插入;② 指定位置插入;对于ArrayList插入方法有4个:

    • public boolean add(E e):在数组尾部插入元素;
    • public void add(int index, E element):在数组指定index位置插入元素;
    • public boolean addAll(Collection<? extends E> c):在数组尾部插入多个元素;
    • public boolean addAll(int index, Collection<? extends E> c):在数组指定index位置插入多个元素;
    public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
    }

     主要关注ensureCapacityInternal方法具体做了什么事情:现有的数组容量是否支持新增元素的需求,如果不满足,则考虑扩容操作

    通过底层源码,我们看看JDK的设计者是如何考虑的:

    STEP-1:首先是ensureCapacityInternal(int minCapacity)方法,该方法的主要功能是 确保数组内部容量足以满足本次插入操作

    该方法实际是首先调用calculateCapacity(Object[] elementData, int minCapacity)方法,计算满足本次插入操作所需要的容量,然后调用ensureExplicitCapacity(int minCapacity)方法,保证容量足够

    /**
    * 确保数组容量足够添加元素
    *
    * @param minCapacity 最小容量
    */
    private void ensureCapacityInternal(int minCapacity) {
        // 计算容量
        int capacity = calculateCapacity(elementData, minCapacity);
        // 确保容量足够 -- 如不足够则触发扩容
        ensureExplicitCapacity(capacity);
    }

    STEP-2:考量calculateCapacity(Object[] elementData, int minCapacity)方法是如何计算本次插入操作所需容量的。

    如果elementData数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA:没有指定初始化容量时;则会判断是最小容量minCapacity和DEFAULT_CAPACITY的大小,取两者中较大者。

    /**
    * 计算容量
    * @param elementData 元素数组
    * @param minCapacity 最小容量
    * @return int 计算后的数组容量大小
    */
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 如果是DEFAULT_CAPACITY ,那么就会从10开
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // DEFAULT_CAPACITY 定义:private static final int DEFAULT_CAPACITY = 10;
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 如果不是则直接返回需要的最小容量
        return minCapacity;
    }

    STEP-3:考量ensureExplicitCapacity 如何确保数组容量足够本次新增操作的:当所需的最小容量minCapacity大于elementData.length数组长度时,则触发grow()执行扩容。

    /**
      * 确保数组剩余容量足够
      * @param minCapacity 最小容量
      */
    private void ensureExplicitCapacity(int minCapacity) {
        // 在父类 AbstractList中定义了 用以记录数组修改的次数(注意是次数)
        modCount++;
    
        // minCapacity代表的是本次(新增)操作所需要的最小容量, elementData.length代表的是当前元素数组的长度
        // 可能写成 minCapacity > elementData.length 更好理解
        if (minCapacity - elementData.length > 0) {
            // 扩容可指定最小的容量(满足当前需求)
            grow(minCapacity);
        }
    }
    /**
      * 扩容机制
      * 旧容量经过运算扩展为1.5倍后与最小容量minCapacity进行比较
      * 如果大于则采用旧容量扩展1.5倍后的大小,否则采用最小容量minCapacity
      * @param minCapacity 新增操作所需最小容量
      */
    private void grow(int minCapacity) {
        // 旧容量大小 elementData数组的长度
        int oldCapacity = elementData.length;
        // 新容量大小 == 旧 + 旧的位运算 (向右移动1位)   大致上是原大小的1.5倍
    // ArrayList扩容机制
    int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果计算出来的新容量小于 指定的最小容量,则创建指定的最小容量大小的新数组 // 可理解为 newCapacity < minCapacity 不知道为啥会喜欢写成减法的形式。。。 if (newCapacity - minCapacity < 0) { newCapacity = minCapacity; } // 如果新容量大于最大值(会出现内存不足的情况) // private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 if (newCapacity - MAX_ARRAY_SIZE > 0) { // hugeCapicaty方法用以当 newCapacity超过指定范围的时候,确保创建的数组不会溢出 newCapacity = hugeCapacity(minCapacity); } // 数组拷贝,将原有的数据搬到新的数组上 elementData = Arrays.copyOf(elementData, newCapacity); }

    综述:经过上述过程的研讨,我们知道ArrayList插入操作的整个过程:首先计算插入元素所需的最小容量;然后判断当前elementData数组是否支持本次插入操作,当容量不足时则触发扩容机制,将原有数组的元素拷贝到新数组上,然后往后追加新的元素。【备注,其他3个插入操作过程基本类似,可自行分析源码】

    查找元素

    public int indexOf(Object o):查找首个为指定元素o的位置,源码如下:

    /**
      * 查找首个为指定元素 o 的位置
      * @param o 被查找元素
      * @return int 如果存在则返回对应index 不存在返回-1
      */
    @Override
    public int indexOf(Object o) {
        // 从这里我们其实也可以看出,ArrayList是支持存储null值的
        if (o == null) {
            for (int i = 0; i < size; i++) {
                if (elementData[i]==null) {
                    return i;
                }
            }
        } else {
            for (int i = 0; i < size; i++) {
                if (o.equals(elementData[i])) {
                    return i;
                }
            }
        }
        // 不存在则返回 -1
        return -1;
    }

    删除元素

    删除元素的方法主要有4个,分别如下:

    • public E remove(int index):移除指定位置的元素,并返回该位置的原元素;
    • public boolean remove(Object o):移除首个为o的元素,并返回是否移除成功;
    • protected void removeRange(int fromIndex, int toIndex):批量移除[fromIndex, toIndex)内的多个元素,注意包左不含右
    • public boolean removeAll(Collection<?> c):批量移除指定的多个元素;

    public E remove(int index) 继承于 AbstractList 抽象类,删除指定位置的元素,并返回该位置上的原元素。

    /**
      * 删除指定位置index的元素,并返回该元素
      * @param index
      * @return E
      */
    public E remove(int index) {
        // index 合法性校验,不合法则抛出相关异常
        rangeCheck(index);
        // 修改数组的次数 + 1
        modCount++;
        // 获取index下标对应的value,elementData方法其实就是 return (E) elementData[index]
        E oldValue = elementData(index);
    
        // 每删除一个元素,都需要对原有的数组进行移动,所以从这里也可以看出ArrayList并不是特别适合于删除操作比较多的场景~
        // 需要移动的元素的数量
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index + 1, elementData, index, numMoved);
        // 数组的最后一个位置设置为null  帮助GC
        elementData[--size] = null;
    
        return oldValue;
    }
    
    private void rangeCheck(int index) {
        // index 合法性校验
        if (index >= size) {
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
    }

    【注意】:删除源码是比较简单易读的,注意一点:凡是涉及到index的操作首先需要参数合法性检查,然后再获取index对应下标的元素,再对index位置后的元素向前移动,最终返回被删除的元素值。

    转换成数组

    • public Object[] toArray():将ArrayList转换为Object[]数组;
    • public <T> T[] toArray(T[] a):将ArrayList转换为指定T泛型的数组;
    /**
      * 将ArrayList转换成Object类型数组
      * @return Object[]
      */
    @Override
    public Object[] toArray() {
        // 返回的是Object[] 类型,需要注意;转换成数组就相当于是将 ArrayList的底层elementData暴露出去而已
        return Arrays.copyOf(elementData, size);
    }
    
    
    public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }
    /**
      * 将ArrayList转换成指定泛型的数组
      * @param a
      * @return T[]
      */
    @Override
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        // 《1》如果传入的数组小于 size 的大小,直接拷贝一个新的数组返回
        if (a.length < size) {
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        }
        // 《2》否则直接拷贝数组即可
        System.arraycopy(elementData, 0, a, 0, size);
        // 额 这个的目的是? 有点没看懂
        if (a.length > size) {
            a[size] = null;
        }
        // 返回传入的a,但是考虑到《1》的时候会返回新的数组,所以即时《2》返回a数组,最好还是按照 a = list.toArray(a); 来使用
        return a;
    }

    【注意】:我们在调用toArray()方法时,可能会遇到异常java.lang.ClassCastException:这是因为toArray()方法返回的类型是Object[],如果我们将其转换为其他类型,可能排除异常。这是因为Java并不支持向下转型,所以当我们需要将ArrayList转换成数组并且指定类型的时候,应该使用指定泛型的方法。

    其他方法

    /**
      * 判断集合中是否含有元素 o
      * 可以看出,contains方法其实就是依赖的 indexOf方法
      * @param o
      * @return boolean
      */
    @Override
    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }
    
    /**
      * 获取指定index位的元素
      * @param index
      * @return E
      */
    @Override
    public E get(int index) {
        // 参数检查
        rangeCheck(index);
    
        // 相当于直接返回 elementData[index]
        return elementData(index);
    }
    
    /**
      * 设置指定index位置的元素,并且返回该index位置旧元素值
      * 并返回旧元素的值(相当于替换)
      * @param index
      * @param element
      * @return E
      */
    @Override
    public E set(int index, E element) {
        // 参数检查
        rangeCheck(index);
        
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
    
    
    /**
      * 清空集合
      */
    @Override
    public void clear() {
        // 数组操作次数 + 1,虽然不知道记录这个东东有啥子用