zoukankan      html  css  js  c++  java
  • ArrayList源码分析

    ArrayList的底层是动态数组,其容量可以动态增长。

    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    
    • RandomAccess 是一个标志接口,表明实现这个这个接口的List集合是支持快速随机访问的。在ArrayList 中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问。
    • ArrayList 实现了Cloneable 接口 ,即覆盖了函数clone(),能被克隆。
    • ArrayList 实现了 java.io.Serializable 接口,这意味着ArrayList支持序列化,能通过序列化去传输。

    ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置i插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。

    属性

    private static final long serialVersionUID = 8683452581122892189L;
    // 集合的默认容量
    private static final int DEFAULT_CAPACITY = 10;
    
    // 用于空对象的共享空数组实例
    // 当用户指定该ArrayList容量为0时,返回该空数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    // 用于默认容量大小空对象的共享空数组实例。当用户没有指定ArrayList的容量时(即调用无参构造函数),返回的是该数组。把它从EMPTY_ELEMENTDATA数组中区分出来,以知道在添加第一个元素时容量需要增加多少。
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    // 保存ArrayList数据的数组
    transient Object[] elementData;
    
    // 当前集合包含的元素数量,也就是集合的实际长度
    private int size;
    

    还有一个从父类AbstractList继承过来的属性modCount,用于记录ArrayList集合的修改次数。结构修改是指更改列表的大小的结构修改,或以其他方式干扰列表进度的迭代可能产生不正确结果的方式。

    protected transient int modCount = 0;
    

    构造函数

    ​ 创建集合时,如果不传入参数,则使用默认无参构建方法创建ArrayList对象,当进行第一次add的时候,elementData将会变成默认的长度:10,即容量扩为10。

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

    也可以在创建集合时,指定ArrayList的初始数组长度,即传入一个int型的参数,传入参数如果是大于0,则使用用户的参数初始化;如果传入的参数等于0,则创建的的是一个空对象的共享空数组实例;如果用户传入的参数小于0,则抛出异常:

    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的时候,传入一个指定的Collection对象,按照它们由集合的迭代器返回的顺序存入数组列表。

    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // 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;
        }
    }
    

    它的具体执行逻辑是:

    ​ 1)将collection对象转换成数组,然后将数组的地址的赋给elementData。

    ​ 2)更新size的值,同时判断size的大小,如果是size等于0,直接将空对象EMPTY_ELEMENTDATA的地址赋给elementData

    ​ 3)如果size的值大于0,则执行Arrays.copy方法,把collection对象的内容(可以理解为深拷贝)copy到elementData中。

    ​ 注意:this.elementData = arg0.toArray(); 这里执行的简单赋值时浅拷贝,所以要执行Arrays,copy做深拷贝

    注意:JDK7 new无参构造的ArrayList对象时,直接创建了长度是10的Object[]数组elementData 。jdk7中的ArrayList的对象的创建类似于单例的饿汉式(先创建好,等待被使用),而jdk8中的ArrayList的对象的创建类似于单例的懒汉式(使用时才创建)。JDK8的内存优化也值得我们在平时开发中学习。

    扩容机制

    add方法

    // 将指定的元素追加到此列表的末尾
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    

    ​ 添加元素之前,先调用ensureCapacityInternal方法,查看一下该方法的底层逻辑:

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 获取默认的容量和传入参数的较大值
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    

    ​ 传入到ensureCapacityInternal方法中的参数值是size加1,即minCapacity = size + 1,这是当前加入一个新元素后所需的容量,该方法也就是要确保当前数组是否有足够容量存下这个minCapacity数量的元素。

    ​ ensureCapacityInternal方法调用ensureExplicitCapacity之前,会先将当前最新的元素数量传入calculateCapacity方法,用于计算当前对象数组所需的容量,然后作为参数传给ensureExplicitCapacity方法。

    ​ 在calculateCapacity方法内部,如果当我们使用无参构造函数创建数组列表时,没有指定容量大小,其内部对象数组的实例是DEFAULTCAPACITY_EMPTY_ELEMENTDATA当我们调用add方法第一次添加元素时,传入的minCapacity为1,在经过Math.max方法比较之后,对象数组所需的容量大小也就变为了10。

    ​ 然后将对象数组的所需的容量大小传给ensureExplicitCapacity方法,用于判断是否需要扩容,如果当前所需的容量大于对象数组的长度,则调用grow方法进行扩容。

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            //调用grow方法进行扩容,调用此方法代表已经开始扩容了
            grow(minCapacity);
    }
    

    ​ 用于标识arraylist修改记录的modcount会自增1,表示集合的结构修改了一次。

    所以add方法的逻辑可以总结为以下几步:

    (1)首先判断数组列表已经使用的长度加上1之后是否足够存下当前这个新元素

    1)如果add添加的是数组列表的第一个元素,此时elementData.length为0 (因为还是一个空的list),此时执行了 ensureCapacityInternal()方法 , minCapacity会被设置为默认容量10。;
    2)如果add添加的不是数组列表的第一个元素,而此时传入ensureCapacityInternal方法的参数值是已有元素数量加1(size+1),那么在calculateCapacity方法中就直接返回的是这个值;
    

    (2)ensureExplicitCapacity方法接收calculateCapacity计算的minCapacity值,与当前对象数组的容量进行比较,如果minCapacity - elementData.length > 0则会进行扩容,否则不扩容。

    (3)将元素e添加到对象数组中。

    ​ 将指定的元素插入此列表中的指定位置。同时将当前位于该位置的元素(如果有的话)和任何它后面位置的元素右移(将它们的索引加一)。

    public void add(int index, E element) {
        rangeCheckForAdd(index);
    
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //arraycopy()方法此时就是将index位置及其之后的元素全部向后挪动1个位置
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
    

    该方法的执行逻辑是:

    ​ 该方法先调用rangeCheckForAdd对index进行界限检查;

    ​ 然后调用ensureCapacityInternal方法保证capacity足够大,同时增加该集合的修改记录数;

    ​ 再将从index开始之后的所有成员后移一个位置;

    ​ 将element插入index位置;最后size加1。

    此处的界限检查函数要保证指定的元素插入位置既要大于0,也要位于小于等于当前数组列表含有的元素数量,其源码如下:

    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    

    此处的size是ArrayList的大小(它包含的元素数)。

    grow方法

    // 要分配的最大数组大小
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    // 给数组列表扩容的核心方法,通过该方法会增加数组列表的容量,以确保至少能够容纳minCapacity参数指定的元素数。
    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);
    }
    
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
    

    ​ 从newCapacity = oldCapacity + (oldCapacity >> 1)这段代码可以看出,每次扩容时,首先采取的方法是将容量变为原先容量的1.5倍左右。进行1.5倍左右的扩容操作之后,会与minCapacity参数进行比较,如果仍小于minCapacity,那么就将minCapacity的值直接赋值给新容量变量。

    ​ 如果新容量大于MAX_ARRAY_SIZE,则进入(执行) hugeCapacity()方法来比较minCapacity和 MAX_ARRAY_SIZE,如果minCapacity大于最大容量MAX_ARRAY_SIZE,则新容量则为Integer.MAX_VALUE,否则,新容量大小则为MAX_ARRAY_SIZE即为Integer.MAX_VALUE - 8。

    System.arraycopy()和Arrays.copyOf()

    System.arraycopy

    从指定的源数组复制数据,从源数组中指定的位置开始复制,将复制以后的数据复制到目标数组的指定位置。

    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);
    

    其参数含义如下:

    src:源数组
    srcPos:要复制源数组中的数据的起始位置
    dest:目标数组
    destPos:将复制的数据放置到目标数组中的起始位置。
    length:要复制的数组元素的数量
    

    Arrays.copyOf

    ​ 复制指定的数组,以空值截断或填充(如有必要),以便副本具有指定的长度。

    public static <T> T[] copyOf(T[] original, int newLength) {
        return (T[]) copyOf(original, newLength, original.getClass());
    }
    
    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;
     }
    

    copyOf有两个参数:

    T[] original 源数组
    int newLength 指定新数组的长度
    

    copyOf底层调用的依旧是System.arraycopy方法,该方法不用调用者指定目标数组,而是自己在底层创建了一个用于存储源数组中被复制数据的数组copy。它进行数组复制时,是将源数组从索引位置为0的地方开始复制,复制到copy中,存储位置也是从0开始。要复制的元素数量是newLength和源数组元素数量二者之间的最小值。最终该方法会返回copy。

    二者区别

    arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置

    copyOf() 是系统自动在内部新建一个数组,并返回该数组。

    案例

    public static void main(String[] args) {
    
    
        int[] a = {1,2,3,4,5};
        int[] b = new int[10];
    
        // System.arraycopy(a,0,b,0,10);
        // java.lang.ArrayIndexOutOfBoundsException
    
        //System.arraycopy(a,0,b,0,5);
        // [1, 2, 3, 4, 5, 0, 0, 0, 0, 0]
    
        System.arraycopy(a,2,b,0,3);
        // [3, 4, 5, 0, 0, 0, 0, 0, 0, 0]
    
        System.out.println(Arrays.toString(b));
    
    
        int[] ints = Arrays.copyOf(a, 6);
    
        System.out.println(ints.length);
        System.out.println(Arrays.toString(ints));
        /*
        6
        [1, 2, 3, 4, 5, 0]
         */   
    }
    

    ​ 使用System.arraycopy方法时,一定要注意源数组中被复制的起始位置与要复制的元素数量之和,即srcPos+length要小于等于源数组的长度,否则就会发生数组越界异常。

    ​ 对于Arrays.copyOf方法,如果要复制的元素数量大于源数组中长度,则会使用数组类型对应的默认值填充。数组的类型是在运行时确定的。

    其他方法

    get

    ​ 获取指定位置上的元素。

    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }
    E elementData(int index) {
        return (E) elementData[index];
    }
    

    set

    参数index的值要大于等于0,小于当前数组列表中的元素数量。获取指定位置(index)元素,然后放到oldValue存放,将需要设置的元素放到指定的位置(index)上,然后将原来位置上的元素oldValue返回给用户。

    public E set(int index, E element) {
        rangeCheck(index);
    
        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }
    
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    

    contains

    ​ 调用indexOf方法,遍历数组中的每一个元素作对比,如果找到对于的元素,则返回true,没有找到则返回false。

    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }
    
    public int indexOf(Object o) {
        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;
        }
        return -1;
    }
    

    remove(int index)

    ​ 删除此列表中指定位置index处的元素, 将index之后的所有元素向左移动(它们的索引都减去1)。

    public E remove(int index) {
        // 判断索引index是否越界,合法范围:0 =< index < size
        rangeCheck(index);
    	// 自增结构修改次数
        modCount++;
        // 将原来在index位置上的值取出来给oldValue
        E oldValue = elementData(index);
    	// 计算出要左移的元素数量
        int numMoved = size - index - 1;
        // 元素左移
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 将最后面的一个元素置空,以便于让垃圾回收器回收
        elementData[--size] = null; // clear to let GC do its work
    
        return oldValue;
    }
    

    注意:调用这个方法不会缩减对象数组的容量,只是将最后一个数组元素置空而已。比如看下面的案例,创建一个ArrayList,添加完三个元素之后,此时ArrayList底层的对象数组elementData的容量应该是默认值10,当采用remove方法移除索引为1的元素之后,elementData数组的长度依旧是10,但是其中的元素数量已经变为2。

    public static void main(String[] args) {
    
        ArrayList<Object> objects = new ArrayList<>();
    
        objects.add(1);
        objects.add(1);
        objects.add(1);
    
        objects.set(0,2);
    
        objects.remove(1);
    
        System.out.println(objects.size());
    }
    

    remove(Object o)

    ​ 循环遍历所有对象,得到对象所在索引位置,然后调用fastRemove方法,执行remove操作。如果该列表中有多个与指定对象相等的元素,那么该方法只会删除列表中出现的第一个与之相等的元素。如果列表不包含该元素,则它不会更改,此时返回false。如果此列表包含指定的元素,则在删除后,会返回true。

    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    
    private void fastRemove(int index) {
        // 自增结构修改次数
        modCount++;
        // 计算要被删除的元素的索引位置
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }
    

    clear

    ​ 添加操作次数(modCount),将数组内的元素都置空,等待垃圾收集器收集,不减小数组容量。

    public void clear() {
        modCount++;
    
        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;
    
        size = 0;
    }
    

    trimToSize方法

    ​ 将elementData中空余的空间(包括null值)去除,例如:数组长度为10,其中只有前三个元素有值,其他为空,那么调用该方法之后,数组的长度变为3.

    public void trimToSize() {
        // 修改结构记录数加1
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }
    

    toArray()

    ​ 以正确的顺序(从第一个到最后一个元素)返回一个包含此列表中所有元素的数组。返回的数组将是“安全的”,因为该列表不保留对它的引用。(换句话说,这个方法必须分配一个新的数组)。因此,调用者可以自由地修改返回的数组。此方法充当基于阵列和基于集合的API之间的桥梁。

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

    需要遍历数组的方法

    ​ clear、remove(Object o)、contains

    参考:https://snailclimb.gitee.io/javaguide/#/docs/java/collection/ArrayList源码+扩容机制分析?id=_34-ensurecapacity方法

  • 相关阅读:
    js定位光标到输入框指定位置
    JS获取本机时间和实时动态时间代码
    一个小游戏
    select optionschange oeder
    js控制下拉列表框
    glow滤镜的使用
    body.innerHTML
    怎样用C语言编写病毒(三)
    2011东北地区赛G题(二分网络流判可行性)
    Codeforces Round #122 (Div. 1)>TLE代码 跪求(n^2)的最小割顶集算法(StoerWagner)
  • 原文地址:https://www.cnblogs.com/yxym2016/p/14539833.html
Copyright © 2011-2022 走看看