zoukankan      html  css  js  c++  java
  • Java集合之ArrayList源码分析

    Java集合之ArrayList源码分析

    个人开发环境

    java环境:Jdk1.8.0_60

    编译器:IntelliJ IDEA 2017.1.4

    源码连接:待整理

    原文链接:https://juejin.im/post/5cb161b3e51d456e7c0cdad4#heading-16

    (个人附加了英文注释,以及为了更好阅读调整了一下,英文不好趁看源码机会冲冲电,理解不当望大佬们指出,仅供个人学习总结使用,如有侵权,联删!!!)

    UML图

    ArrayList

    源码解析

    属性

    /**
     * Default initial capacity.
     * 默认初始化容量为 10
     */
    private static final int DEFAULT_CAPACITY = 10;
    
    /**
     * Shared empty array instance used for empty instances.
     * 一个空数组,当用户指定该ArrayList为0时,返回该空数组
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};
    
    /**
     * Shared empty array instance used for default sized empty instances.
     * We distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
     * first element is added.
     * 用户没有指定ArrayList的容量时(即调用无参构造函数),返回该数组,其容量为0,
     * 当用户第一次添加于元素时,该数组将会扩容,变成默认容量为10(使用 DEFAULT_CAPACITY)
     * <p>
     * EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 区别:
     * EMPTY_ELEMENTDATA:new ArrayList<E>(0);
     * DEFAULTCAPACITY_EMPTY_ELEMENTDATA:new ArrayList<E>();
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     * <p>
     * 一个数组缓冲区,ArrayList基于该数组实现,ArrayList容量就是该数组的长度
     * 该值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,当第一次添加元素进入ArrayList中时,
     * 数组将扩容至 DEFAULT_CAPACITY = 10
     */
    transient Object[] elementData; // non-private to simplify nested class access
    
    /**
     * The size of the ArrayList (the number of elements it contains).
     * ArrayList包含元素的个数
     */
    private int size;
    
    • **DEFAULT_CAPACITY **
      • 默认容量为10,也就是通过new ArrayList()创建时的默认容量。
    • EMPTY_ELEMENTDATA
      • 空的数组,这种是通过new ArrayList(0)创建时用的是这个空数组。
    • DEFAULTCAPACITY_EMPTY_ELEMENTDATA
      • 一个数组缓冲区,ArrayList基于该数组实现,ArrayList容量就是该数组的长度,该值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,当第一次添加元素进入ArrayList中时,数组将扩容至 DEFAULT_CAPACITY = 10
    • elementData
      • 真正存放元素的地方,使用transient是为了不序列化这个字段。
    • size
      • 真正存储元素的个数,而不是elementData数组的长度

    ArrayList(int initialCapacity)构造方法

    传入初始容量,如果大于0就初始化elementData为对应大小,如果等于0就使用EMPTY_ELEMENTDATA空数组,如果小于0抛出异常。

    /**
     * Constructs an empty list with the specified initial capacity.
     * 创建一个指定容量的集合
     *
     * @param initialCapacity 初始容量
     * @throws IllegalArgumentException 当初始容量为负值时,抛出非法容量异常
     */
    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()构造方法

    不传初始容量,初始化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA空数组,会在添加第一个元素的时候扩容为默认的大小,DEFAULT_CAPACITY = 10。

    /**
     * Constructs an empty list with an initial capacity of ten.
     * 默认空参构造函数:使用空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * 使用这个数组是在添加第一个元素的时候会扩容到默认大小10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    

    ArrayList(Collection<? extends E> c)构造方法

    传入集合并初始化elementData,这里会使用拷贝把传入集合的元素拷贝到elementData数组中,如果元素个数为0,则初始化为EMPTY_ELEMENTDATA空数组。

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     * 创建一个包含Collection的ArrayList
     *
     * @param c 将 collection 的元素存入到新建的ArrayList中
     * @throws NullPointerException 如果 c 为空 ,则抛出异常
     */
    public ArrayList(Collection<? extends E> c) {
        // 将集合转换为Object[]数组
        elementData = c.toArray();
        // 将转换后的Object[]数组长度赋值给当前ArrayList的size,判断是否为0
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[]
            // 转换后的数组可能不会返回Object[]
            if (elementData.getClass() != Object[].class)
                // 若elementData不是Object[]类型,则复制指定数组内容,构造成大小为size的Object[]数组
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            // 替换为空数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
    

    为什么c.toArray();返回的有可能不是Object[]类型呢?请看下面的代码:

    public class ArrayTest {
        public static void main(String[] args) {
            Father[] fathers = new Son[]{};
            // 打印结果为class [Lcom.coolcoding.code.Son;
            System.out.println(fathers.getClass());
    
            List<String> strList = new MyList();
            // 打印结果为class [Ljava.lang.String;
            System.out.println(strList.toArray().getClass());
        }
    }
    
    class Father {}
    
    class Son extends Father {}
    
    class MyList extends ArrayList<String> {
        /**
         * 子类重写父类的方法,返回值可以不一样
         * 但这里只能用数组类型,换成Object就不行
         * 应该算是java本身的bug
         */
        @Override
        public String[] toArray() {
            // 为了方便举例直接写死
            return new String[]{"1", "2", "3"};
        }
    }
    

    add(E e)方法

    添加元素到末尾,平均时间复杂度为O(1)。

    public boolean add(E e) {
        // 检查是否需要扩容
        ensureCapacityInternal(size + 1);
        // 把元素插入到最后一位
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 如果是空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA,就初始化为默认大小10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
    
        if (minCapacity - elementData.length > 0)
            // 扩容
            grow(minCapacity);
    }
    
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        // 新容量为旧容量的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 如果新容量发现比需要的容量还小,则以需要的容量为准
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 如果新容量已经超过最大容量了,则使用最大容量
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 以新容量拷贝出来一个新数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    • add(E e)方法执行流程总结
      • 1)检查是否需要扩容;
      • 2)如果elementData等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA则初始化容量大小为DEFAULT_CAPACITY;
      • 3)新容量是老容量的1.5倍(oldCapacity + (oldCapacity >> 1)),如果加了这么多容量发现比需要的容量还小,则以需要的容量为准;
      • 4)创建新容量的数组并把老数组拷贝到新数组;

    add(int index, E element)方法

    添加元素到指定位置,平均时间复杂度为O(n)。

    public void add(int index, E element) {
        // 检查是否越界
        rangeCheckForAdd(index);
        // 检查是否需要扩容
        ensureCapacityInternal(size + 1);
        // 将inex及其之后的元素往后挪一位,则index位置处就空出来了
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 将元素插入到index的位置
        elementData[index] = element;
        // 大小增1
        size++;
    }
    
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    • add(int index, E element)方法执行流程总结

      • 1)检查索引是否越界;

      • 2)检查是否需要扩容;

      • 3)把插入索引位置后的元素都往后挪一位;

      • 4)在插入索引位置放置插入的元素;

      • 5)大小加1;

    addAll(Collection<? extends E> c)方法

    求两个集合的并集。

    /**
    * 将集合c中所有元素添加到当前ArrayList中
    */
    public boolean addAll(Collection<? extends E> c) {
        // 将集合c转为数组
        Object[] a = c.toArray();
        int numNew = a.length;
        // 检查是否需要扩容
        ensureCapacityInternal(size + numNew);
        // 将c中元素全部拷贝到数组的最后
        System.arraycopy(a, 0, elementData, size, numNew);
        // 大小增加c的大小
        size += numNew;
        // 如果c不为空就返回true,否则返回false
        return numNew != 0;
    }
    
    • addAll(Collection<? extends E> c)方法执行流程总结
      • 1)拷贝c中的元素到数组a中;
      • 2)检查是否需要扩容;
      • 3)把数组a中的元素拷贝到elementData的尾部;

    get(int index)方法

    获取指定索引位置的元素,时间复杂度为O(1)。

    public E get(int index) {
        // 检查是否越界
        rangeCheck(index);
        // 返回数组index位置的元素
        return elementData(index);
    }
    
    private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    
    E elementData(int index) {
        return (E) elementData[index];
    }
    
    • get(int index)方法执行流程总结
      • 1)检查索引是否越界,这里只检查是否越上界,如果越上界抛出IndexOutOfBoundsException异常,如果越下界抛出的是ArrayIndexOutOfBoundsException异常。
      • 2)返回索引位置处的元素;

    remove(int index)方法

    删除指定索引位置的元素,时间复杂度为O(n)。

    public E remove(int index) {
        // 检查是否越界
        rangeCheck(index);
    
        modCount++;
        // 获取index位置的元素
        E oldValue = elementData(index);
        
        // 如果index不是最后一位,则将index之后的元素往前挪一位
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index, numMoved);
        
        // 将最后一个元素删除,帮助GC
        elementData[--size] = null; // clear to let GC do its work
    
        // 返回旧值
        return oldValue;
    }
    
    • remove(int index)方法执行流程总结
      • 1)检查索引是否越界;
      • 2)获取指定索引位置的元素;
      • 3)如果删除的不是最后一位,则其它元素往前移一位;
      • 4)将最后一位置为null,方便GC回收;
      • 5)返回删除的元素。

    可以看到,ArrayList删除元素的时候并没有缩容。

    remove(Object o)方法

    删除指定元素值的元素,时间复杂度为O(n)。

    public boolean remove(Object o) {
        if (o == null) {
            // 遍历整个数组,找到元素第一次出现的位置,并将其快速删除
            for (int index = 0; index < size; index++)
                // 如果要删除的元素为null,则以null进行比较,使用==
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            // 遍历整个数组,找到元素第一次出现的位置,并将其快速删除
            for (int index = 0; index < size; index++)
                // 如果要删除的元素不为null,则进行比较,使用equals()方法
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
    
    private void fastRemove(int index) {
        // 少了一个越界的检查
        modCount++;
        // 如果index不是最后一位,则将index之后的元素往前挪一位
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index, numMoved);
        // 将最后一个元素删除,帮助GC
        elementData[--size] = null; // clear to let GC do its work
    }
    
    • remove(Object o)方法

      • 1)找到第一个等于指定元素值的元素;
      • 2)快速删除;

    fastRemove(int index)相对于remove(int index)少了检查索引越界的操作,可见jdk将性能优化到极致。

    retainAll(Collection<?> c)方法

    求两个集合的交集。

    public boolean retainAll(Collection<?> c) {
        // 集合c不能为null
        Objects.requireNonNull(c);
        // 调用批量删除方法,这时complement传入true,表示删除不包含在c中的元素
        return batchRemove(c, true);
    }
    
    /**
    * 批量删除元素
    * complement为true表示删除c中不包含的元素
    * complement为false表示删除c中包含的元素
    */
    private boolean batchRemove(Collection<?> c, boolean complement) {
        final Object[] elementData = this.elementData;
        // 使用读写两个指针同时遍历数组
        // 读指针每次自增1,写指针放入元素的时候才加1
        // 这样不需要额外的空间,只需要在原有的数组上操作就可以了
        int r = 0, w = 0;
        boolean modified = false;
        try {
            // 遍历整个数组,如果c中包含该元素,则把该元素放到写指针的位置(以complement为准)
            for (; r < size; r++)
                if (c.contains(elementData[r]) == complement)
                    elementData[w++] = elementData[r];
        } finally {
            // 正常来说r最后是等于size的,除非c.contains()抛出了异常
            if (r != size) {
                // 如果c.contains()抛出了异常,则把未读的元素都拷贝到写指针之后
                System.arraycopy(elementData, r,
                                 elementData, w,
                                 size - r);
                w += size - r;
            }
            if (w != size) {
                // 将写指针之后的元素置为空,帮助GC
                for (int i = w; i < size; i++)
                    elementData[i] = null;
                modCount += size - w;
                // 新大小等于写指针的位置(因为每写一次写指针就加1,所以新大小正好等于写指针的位置)
                size = w;
                modified = true;
            }
        }
        // 有修改返回true
        return modified;
    }
    
    • retainAll(Collection<?> c)方法

      • 1)遍历elementData数组;
      • 2)如果元素在c中,则把这个元素添加到elementData数组的w位置并将w位置往后移一位;
      • 3)遍历完之后,w之前的元素都是两者共有的,w之后(包含)的元素不是两者共有的;
      • 4)将w之后(包含)的元素置为null,方便GC回收;

    removeAll(Collection<?> c)

    求两个集合的单方向差集,只保留当前集合中不在c中的元素,不保留在c中不在当前集体中的元素。

    public boolean removeAll(Collection<?> c) {
        // 集合c不能为空
        Objects.requireNonNull(c);
        // 同样调用批量删除方法,这时complement传入false,表示删除包含在c中的元素
        return batchRemove(c, false);
    }
    

    与retainAll(Collection<?> c)方法类似,只是这里保留的是不在c中的元素。

    总结

    (1)ArrayList内部使用数组存储元素,当数组长度不够时进行扩容,每次加一半的空间,ArrayList不会进行缩容;

    (2)ArrayList支持随机访问,通过索引访问元素极快,时间复杂度为O(1);

    (3)ArrayList添加元素到尾部极快,平均时间复杂度为O(1);

    (4)ArrayList添加元素到中间比较慢,因为要搬移元素,平均时间复杂度为O(n);

    (5)ArrayList从尾部删除元素极快,时间复杂度为O(1);

    (6)ArrayList从中间删除元素比较慢,因为要搬移元素,平均时间复杂度为O(n);

    (7)ArrayList支持求并集,调用addAll(Collection<? extends E> c)方法即可;

    (8)ArrayList支持求交集,调用retainAll(Collection<? extends E> c)方法即可;

    (7)ArrayList支持求单向差集,调用removeAll(Collection<? extends E> c)方法即可;

  • 相关阅读:
    [转]Intellij IDEA快捷键与使用小技巧
    Swoole来实现实时异步任务队列
    php 异步执行脚本
    Centos 7 systemctl和防火墙firewalld命令
    tgz的解压
    error: C++ preprocessor "/lib/cpp" fails sanity check错误解决方法
    Linux 命令详解(三)./configure、make、make install 命令
    LNMP, CentOS7.0+Nginx+Mysql5.7+PHP7环境安装
    phpmailer使用qq邮箱、163邮箱成功发送邮件实例代码
    Mibew Messenger (also known as Open Web Messenger)
  • 原文地址:https://www.cnblogs.com/yoci/p/11419911.html
Copyright © 2011-2022 走看看