• Java集合框架——jdk 1.8 ArrayList 源码解析


    前言:作为菜鸟,需要经常回头巩固一下基础知识,今天看看 jdk 1.8 的源码,这里记录 ArrayList 的实现。

    一、简介

      ArrayList 是有序的集合;

      底层采用数组实现对数据的增删查改;

      不是线程安全的;

      有自动扩容的功能。

    二、类图

     

    三、详细总结

      1、ArrayList 是实现了 List 接口的可变数据,非同步实现,并允许包括 null 在内的所有元素。

      2、底层采用数组实现。

      3、在数组增加时,会进行扩容,但由于底层采用的数组实现,所以扩容时会将老数组中的元素拷贝到一份新的数组中,所以性能代价很高

      4、采用了 Fail-Fast 机制,面对并发的修改时,迭代器会抛出异常,而不是冒着在将来某个不确定时间发生任意不确定行为的风险

      5、remove 方法会通过 System.arraycopy() 方法让下标到数组末尾的元素向前移动一个单位,并把最后一位的值置空,方便 GC。

    四、解惑

    1、为什么成员变量 elementData 为什么被 transient 修饰?难道序列化时不需要数组元素?

      参考:https://blog.csdn.net/zero__007/article/details/52166306

      transient 用来表示一个域不是该对象序行化的一部分,当一个对象被序行化的时候,transient 修饰的变量的值是不包括在序行化的表示中的。但是 ArrayList 又是可序行化的类,elementData 是 ArrayList 具体存放元素的成员,用 transient 来修饰 elementData,岂不是反序列化后的 ArrayList 丢失了原先的元素?
           其实玄机在于 ArrayList 中的两个方法:

      /**
         * Save the state of the <tt>ArrayList</tt> instance to a stream (that
         * is, serialize it).
         *
         * @serialData The length of the array backing the <tt>ArrayList</tt>
         *             instance is emitted (int), followed by all of its elements
         *             (each an <tt>Object</tt>) in the proper order.
         */
        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 behavioural 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();
            }
        }
      /**
         * Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
         * deserialize it).
         */
        private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
            elementData = EMPTY_ELEMENTDATA;
    
            // Read in size, and any hidden stuff
            s.defaultReadObject();
    
            // Read in capacity
            s.readInt(); // ignored
    
            if (size > 0) {
                // be like clone(), allocate array based upon size not capacity
                ensureCapacityInternal(size);
    
                Object[] a = elementData;
                // Read in all elements in the proper order.
                for (int i=0; i<size; i++) {
                    a[i] = s.readObject();
                }
            }
        }

      ArrayList 在序列化的时候会调用 writeObject,直接将 size 和 element 写入 ObjectOutputStream;反序列化时调用 readObject,从 ObjectInputStream 获取 size 和 element,再恢复到 elementData。
           为什么不直接用 elementData 来序列化,而采用上诉的方式来实现序列化呢?原因在于 elementData 是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。

    2、为什么有两个默认空数组的成员变量?为什么 new ArrayList() 注释说初始化容量为 10?

      两个虽然都为空数组,但用途稍微有点不一致。

      其中,EMPTY_ELEMENTDATA 用于构造器中给出了初始化容量为 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);
            }
        }

      其中,DEFAULTCAPACITY_EMPTY_ELEMENTDATA  用于默认构造器的数组,主要用于在第一次增加元素时判断是否需要给出默认容量 10 的大小(grow() 方法用于扩容)。这里之所以不直接 new 一个初始容量为 10 的数组,我想是因为有时我们会 new 一个 ArrayList(),但是并不会添加数据,这样就可以节约空间。代码如下:

      public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    
        private void ensureCapacityInternal(int minCapacity) {
         // 判断是否是默认构造函数构造的默认空数组实例,如果是就给出默认容量 10 和 size + 1 的最大值
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
            }
    
            // 根据需要的最小容量来判断是否需要扩容
            ensureExplicitCapacity(minCapacity);
        }
    
        private void ensureExplicitCapacity(int minCapacity) {
            // 快速报错机制
            modCount++;
    
            // overflow-conscious code
            // 如果修改后需要的最小容量大于当前数据的容量时就需要扩容
            if (minCapacity - elementData.length > 0)
                // 开始扩容
                grow(minCapacity);
        }        

    3、如何扩容的? 

      源码中,每次在 add() 一个元素时,ArrayList 都需要对这个 List 的容量进行一个判断。如果容量够,直接添加,否则需要进行扩容,调用 grow() 方法。扩容调用的是 grow() 方法,通过 grow() 方法中调用的 Arrays.copyof() 方法进行对原数组的复制,再通过调用 System.arraycopy() 方法进行复制,达到扩容的目的。

      源码中,可以看出有三种情况:(这里参考 https://blog.csdn.net/u010890358/article/details/80515284)

      (一)如果当前数组是由默认构造方法生成的空数组并且第一次添加数据。此时 minCapacity 等于默认的容量(10),那么根据源码中的逻辑可以看到最后数组的容量会从 0 扩容成 10。而以后的扩容按照当前容量的1.5 倍进行扩容。1.5 倍这里用了右移一位,不明白的可以自行百度。

      (二)如果当前数组是由自定义初始容量构造方法创建并且指定初始容量为 0。此时 minCapacity 等于 1,newCapacity = 0,那么根据下面逻辑可以看到最后数组的容量会从0变成1。这边可以看到一个严重的问题,一旦我们执行了初始容量为 0,那么根据下面的算法前四次扩容每次都 +1,在第5次添加数据进行扩容的时候才是按照当前容量的1.5倍进行扩容。

      (三)如果当扩容量(newCapacity)大于 ArrayList 数组定义的最大值后会调用 hugeCapacity 来进行判断。如果 minCapacity 已经大于 Integer 的最大值(溢出为负数)那么抛出 OutOfMemoryError(内存溢出)否则的话根据与 MAX_ARRAY_SIZE 的比较情况确定是返回 Integer 最大值还是 MAX_ARRAY_SIZE。这边也可以看到 ArrayList 允许的最大容量就是 Integer 的最大值(-2 的 31 次方~ 2 的 31 次方减 1)。

      源码如下:

        //ArrayList 扩容的核心方法,此方法用来决定扩容量并扩容
        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            // 扩容的大小一般为当前容量的 1.5 倍
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                //当扩容量(newCapacity)大于ArrayList数组定义的最大值后会调用hugeCapacity来进行判断。如果minCapacity已经大于Integer的最大值(溢出为负数)那么抛出OutOfMemoryError(内存溢出)否则的话根据与MAX_ARRAY_SIZE的比较情况确定是返回Integer最大值还是MAX_ARRAY_SIZE。这边也可以看到ArrayList允许的最大容量就是Integer的最大值(-2的31次方~2的31次方减1)。
                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;
        }
     
        // ArrayList 的成员变量
        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
     

     4、Java 容器的快速报错机制 fail-fast 是什么?    

      请移步: Java集合框架——容器的快速报错机制 fail-fast 是什么?

    5、System.arraycopy 怎么使用的?

      请移步: System.arraycopy 怎么使用的?

    五、源码解析

    1、主要成员变量

        //默认的初始化容量
        private static final int DEFAULT_CAPACITY = 10;
    
        //空数组,用于 使用构造器给出初始容量为0时的默认空数组
        private static final Object[] EMPTY_ELEMENTDATA = {};
    
        //默认的数组,用于 使用默认构造器创建的默认空数组,主要用于后面第一次增加数据时判断是否需要给出默认容量 10 的大小
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
        //用于存储 ArrayList 的元素,这里就可以看出 ArrayList 的底层就是数组
        transient Object[] elementData; // non-private to simplify nested class access
    
        //大小
        private int size;
    
        //记录被修改的次数,用于迭代器迭代时保证数据没有被修改过
        protected transient int modCount = 0;
    
        //数组大小的最大值
        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    2、构造方法(3个)

      默认构造方法:

      注释说实例化了一个容量为 10 的数组,但其实这里返回的是一个空数组,是在数组第一次增加数据时通过扩容达到的初始容量为 10 的数组。前面解惑的2、为什么有两个默认空数组的成员变量?也提到了。 

        /**
         * Constructs an empty list with an initial capacity of ten.
         */
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }

      自定义初始容量的构造方法:

      /**
         * 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) {
            if (initialCapacity > 0) {
                this.elementData = new Object[initialCapacity];
            } else if (initialCapacity == 0) {
                this.elementData = EMPTY_ELEMENTDATA;
            } else {
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            }
        }

      生成一个带数据的 ArrayList 实例:

      /**
         * Constructs a list containing the elements of the specified
         * collection, in the order they are returned by the collection's
         * iterator.
         *
         * @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();
            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;
            }
        }

    参考:

    https://blog.csdn.net/u010890358/article/details/80515284

    所有的集合框架:

    http://www.runoob.com/java/java-collections.html

    https://blog.csdn.net/qq_25868207/article/details/55259978

  • 相关阅读:
    [AngularFire 2] Protect Write Access Using Security Rules
    [Ramda] Handle Branching Logic with Ramda's Conditional Functions
    [Angular2 Router] Setup page title with Router events
    [HTML] Creating visual skip links in HTML and CSS
    [AngularFire2] Signup and logout
    [AngularFire2] Building an Authentication Observable Data Service
    [Swift] Add Scroll View
    [Swift] Storyboard outlet and action
    [AngularFire2] Auth with Firebase auth -- email
    网站优化与Cdn文件传输服务
  • 原文地址:https://www.cnblogs.com/yuxiaole/p/9634380.html
Copyright © 2020-2023  润新知