美文网首页集合源码深度剖析
集合源码解析之ArrayList

集合源码解析之ArrayList

作者: 可苯 | 来源:发表于2020-08-24 07:56 被阅读0次
            ArrayList是开发中最常使用到的集合,要想深入了解绕不过源码,本篇文章简单讲解其源码,领略ArrayList的风采...
                                                                           ps: 本文以openjdk8为主.
    

    概述

    1. ArrayList是一种可以动态操作的集合类,基于数组实现.
    2. ArrayList允许空值和重复元素,该类封装了一个动态再分配的Obj数组.
    3. 当往ArrayList里添加元素数量大于其底层数组容量时,会通过扩容机制重新生成一个更大的数组.
    4. ArrayList是非线程安全的,多线程操作下需要手动保证该集合类的同步性

    源码分析

    继承结构

    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
    

    ArrayList实现了List<E>``RandomAccess``Cloneable``java.io.Serializable接口,继承了AbstractList类.其中RandomAccess代表了它具备快速随机访问的能力.由于它是基于数组实现,天生带下标,可以实现O(1)复杂度的随机访问.

    为什么要让AbstractList先实现List<E>,然后在让ArrayList继承AbstractList?为什么不让ArrayList直接实现List<E>?

    ArrayList继承AbstractList是一种思想, 现在AbstractList里实现一些通用的方法,而具体的实现类ArrayList继承AbstractList,拿到通用的方法,再实现一些自己特定的方法.这样就算后面有多个List实现,都可以通过继承AbstractList来获取通用方法,减少代码的重复.

    既然ArrayList已经继承了AbstractList,而AbstractList已经实现了List<E>,那么为什么ArrayList还要再实现List<E>?

    有道翻译:
    这是记住此类真正实现该接口的一种方法。
    它不会有任何不良影响,并且可以帮助您理解代码,而无需遍历给定类的完整层次结构。

    另一种可能:

        public static void main(String[] args) {
            System.out.println("Test1: " + Arrays.toString(Test1.class.getInterfaces()));
            System.out.println("Test2: " + Arrays.toString(Test2.class.getInterfaces()));
        }
    
        interface TestInterface { void test(); }
        abstract class AbstractTestInterface implements TestInterface {}
        class Test1 extends AbstractTestInterface {
            @Override
            public void test() { System.out.println("test1"); }
        }
    
        class Test2 extends AbstractTestInterface implements TestInterface {
            @Override
            public void test() {System.out.println("test2"); }
        }
    

    输出结果:


    从结果就可以发现,因为Test1本身没有再次实现接口,因此在获取Test1的接口时为空数组.
    所以在实现代理时就会报错,如下:

        public static void main(String[] args) {
            new Test().testProxy();
        }
    
        private void testProxy() {
            try {
                TestInterface proxy1 = createProxy(new Test1());
                proxy1.test();
            } catch (Exception e) {
                e.printStackTrace();
            }
            TestInterface proxy2 = createProxy(new Test2());
            proxy2.test();
        }
    
        private static <T> T createProxy(final T obj) {
            return (T) Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj
                    .getClass().getInterfaces(), (proxy, method, args) -> method.invoke(obj, args));
        }
    

    在生成动态代理时调用class.getInterfaces(), 不实现接口调用函数会报错.


    可能作者是为了更方便实现代理而精心设计的.

        ps: Set和Map集合同理...
    

    类中属性

    属性比较少,直接看源码

        /** 版本号,用于校验正反序列化时的一致性 */
        private static final long serialVersionUID = 8683452581122892189L;
    
        /** 默认容量 */
        private static final int DEFAULT_CAPACITY = 10;
    
        /** 空对象数组 */
        private static final Object[] EMPTY_ELEMENTDATA = {};
    
        /** 默认空对象数组 */
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
        /** ArrayList的核心,数据都是存在这的,注意这个数组自定义了序列化函数 */
        transient Object[] elementData;
    
        /** 实际元素数量 */
        private int size;
    
        /** 数组的最大容量
         * 尝试分配更大的数组可能会导致OutOfMemoryError:请求的数组大小超过VM限制
         */
        private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
        
        /** AbstractList里的属性, 记录集合 涉及到结构变化的次数(add/remove/clear等) */
        protected transient int modCount = 0;
    

    构造函数

    ArrayList有三个构造函数:

    • 无参构造器
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
    

    将elementData赋值为默认空数组, 实际上jdk8里的ArrayList进行了延迟初始化==(具体初始化在后面解释)==,而jdk8之前的版本是直接进行初始化

        // by openjdk7
        public ArrayList() {
        // 调用有参构造器进行初始化默认容量
            this(10);
        }
    
    • 有参构造器之一
      没什么逻辑,直接上源码
        public ArrayList(int initialCapacity) {
            // 判断下这个初始容量是否大于0
            if (initialCapacity > 0) {
                this.elementData = new Object[initialCapacity];
            } else if (initialCapacity == 0) {
                // 延迟初始化
                this.elementData = EMPTY_ELEMENTDATA;
            } else {
                throw new IllegalArgumentException("Illegal Capacity: " +
                        initialCapacity);
            }
        }
    
    • 有参构造器之二
        public ArrayList(Collection<? extends E> c) {
            // c.toArray可能返回的不是Object对象  see 6260652
            elementData = c.toArray();
    
            if ((size = elementData.length) != 0) {
                if (elementData.getClass() != Object[].class)
                    elementData = Arrays.copyOf(elementData, size, Object[].class);
            } else {
                this.elementData = EMPTY_ELEMENTDATA;
            }
        }
    

    这里涉及到JDK bug库6260652编号,附上官网链接,看下述例子:

        class A {}
        class B extends A {}
        public void Test() {
            B[] b = {new B(), new B()};
            // 子类数组转换成父类数组是允许,向上转型
            A[] a = b;
            System.out.println(a[0]);
            // 由于存储的都是子类型B,java是不允许向下转型的,因此这里会报错ArrayStoreException
            a[0] = new A();
        }
    

    这里说明假如我们有个Obj数组,并不代表着我们可以把Obj对象存进去,这取决于数组中存储元素的实际类型.
    回到源码c.toArray()可能会发生类似于上述例子的A[] a = b,为了防止这种情况,源码中进行了if判断来防止错误的数组对象类型导致异常,Arrays.copyOf(elementData, size, Object[].class)重新创建了个Obj数组,可以任意存储对象.

    核心函数

    添加函数

        // 末尾添加一个元素
        public boolean add(E e);
        
        // 指定下标位置添加一个元素
        public void add(int index, E element);
    
    boolean add(E e)
        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            // size++ 真实数据量+1
            elementData[size++] = e;
            return true;
        }
    

    ensureCapacityInternal是检查扩容函数,size + 1添加前先检查下数组中是否能放下.

        /**
         * 检测扩容时调用的中间方法
         *
         * @param minCapacity
         */
        private void ensureCapacityInternal(int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
            }
    
            ensureExplicitCapacity(minCapacity);
        }
    
    1. 先确认最小容量:elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA即判断初始化的elementData是不是空数组,然后找出默认容量和参数容量大的.对应无参构造器
      延迟初始化
    2. 调用ensureExplicitCapacity函数,此函数才是判断容量是否够用的函数,如果不够用才会调用grow来进行扩容
        private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
            // overflow conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
    

    ensureExplicitCapacity里只有检测到真实容量不够的时候才会扩容, 而grow才是真实扩容的函数,而modCount属性作用比较泛,例如fast fail检查,后续会讲到.

        private void grow(int minCapacity) {
            // 复制扩容前的容量
            int oldCapacity = elementData.length;
    
            // 生成新的大小  每次扩容为原有大小的1.5倍
            int newCapacity = oldCapacity + (oldCapacity >> 1);
    
            // 这里适用于elementData为空数组的时候,即数组初始化时
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
    
            // 当newCapacity超过最大限制,调用hugeCapacity把最大值赋予当newCapacity超过最大限制
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
    
            // 开始进行 扩容
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
        
            private static int hugeCapacity(int minCapacity) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            // 超过最大限,实际上还是把int最大值给了,再大就溢出了,但给予MAX_VALUE,vm就超过限制了..
            return (minCapacity > MAX_ARRAY_SIZE) ?
                    Integer.MAX_VALUE :
                    MAX_ARRAY_SIZE;
        }
    
    void add(int index, E element)
        public void add(int index, E element) {
            rangeCheckForAdd(index); // check index
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            System.arraycopy(elementData, index, elementData, index + 1,
                    size - index);
            elementData[index] = element;
            size++;
        }
        
        private void rangeCheckForAdd(int index) {
            if (index > size || index < 0)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
    

    在指定位置插入时,第一步先调用rangeCheckForAdd来进行越界检查,触发扩容机制ensureCapacityInternal.
    再调用System.arraycopyindex后所有元素往后挪一位.==ArrayList随机插入的效率慢的原因之一==

    注意: 当调用无参构造器进行构建ArrayList时,容量会在第一次调用add函数时进行初始化.
    

    获取函数

    较为简单,直接上源码

        public E get(int index) {
            // 检测是否下标越界
            rangeCheck(index);
    
            // 获取元素
            return elementData(index);
        }
    
        private void rangeCheck(int index) {
            if (index >= size)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
        
        E elementData(int index) { 
            // 内部使用时以Obj, 获取时进行向下转型 Object -> element
            return (E) elementData[index]; 
        }
    

    修改函数

    较为更加简单

        public E set(int index, E element) {
            rangeCheck(index);
            E oldValue = elementData(index);
            elementData[index] = element;
            return oldValue;
        }
    

    删除函数

        // 根据下标移除元素
        E remove(int index);
        
        // 删除列表指定的元素
        boolean remove(Object o); 
        
        // 清空list
        void clear();
    
    E remove(int index)
        public E remove(int index) {
            rangeCheck(index);
            modCount++;
            E oldValue = elementData(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)整体逻辑就是移动元素,把index下面的往前移动一位,再清理最后一位多余的,把被删除的元素返回.

    boolean remove(Object o)
        public boolean remove(Object o) {
            if (o == null) {
                // null值的删除依赖 ==
                for (int index = 0; index < size; index++)
                    if (elementData[index] == null) {
                        fastRemove(index);
                        return true;
                    }
            } else {
                // 非null值的删除依赖 equals
                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);
    
            // 删除最后一个多余的,方便gc
            elementData[--size] = null; // clear to let GC do its work
        }
    

    remove(Object o)逻辑和remove(int index)差不多,相对多了个步骤寻找下标,if (elementData[index] == null)这里可以看出,ArrayList是支持存入null值的.

    void clear();
        public void clear() {
            modCount++;
            for (int i = 0; i < size; i++)
                elementData[i] = null;
    
            size = 0;
        }
    

    把所有元素置为null方便gc,再重置实际元素数量.

    Iterator迭代器

        private class Itr implements Iterator<E> {
            int cursor;       // 要返回的下一个元素的下标
            int lastRet = -1; // 上一次返回的元素 (删除的标志位)
            int expectedModCount = modCount; //用于判断集合是否修改过结构的标志
            
            boolean hasNext();
            
            E next();
    
            void remove();
    
            void forEachRemaining(Consumer<? super E> consumer);
        }
    

    我们都知道Java中的foreach是个语法糖,编译成字节码后会被转成用迭代器遍历的方式.
    来看看Itr,它是ArrayList内部维护的迭代器.

        for (String s : list) {
            System.out.println(s);
        }
        // 等价于
        Iterator var2 = list.iterator();
        while(var2.hasNext()) {
            String s = (String)var2.next();
            System.out.println(s);
        }
    

    来看看hasNextnext函数

        public boolean hasNext() {
            // 判断是否到结尾
            return cursor != size;
        }
    
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size) // 判断是否越界
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) // 再次判断越界,防止异步线程修改List
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i]; // 返回元素,并设置上一次返回的元素的下标
        }
        
        final void checkForComodification() {
            // fast-fail检测, 如若在迭代的同时进行
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    

    next首先会触发checkForComodification,来检测在遍历的过程中是否其他线程修改了list并抛出异常,这就是fast-fail==快速失败策略==
    通过判断modCount和expectedModCount是否一样来判断是否有进行修改.

    因此在foreach时不能对元素进行add/remove操作,remove元素请使用Iterator方式.并发操作,需要对Iterator对象加锁.

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
    
            try {
                ArrayList.this.remove(lastRet); // 删除 modCount会发生变化
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount; // 需同步变化
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
    
    快速失败机制

    ArrayList 迭代器中的方法都是均具有快速失败的特性,当遇到并发修改的情况时,迭代器会快速失败,以避免程序在将来不确定的时间里出现不确定的行为。

    总结

    • ArrayList本质上就是一个Object[]数组
    • ArrayList允许插入null,每次扩容1.5倍
    • ArrayList是通过System.arrayCopyArrays.copyOf来完成大部分操作的
    • ArrayList使用了fast-fail机制,iterator迭代时,如若add/remove时,每次迭代都会检测modCount是否有改变抛异常
    • ArrayList由于本质上是数组,所以它在数据查询方便会很快,在随机插入/删除性能会下降很多,因为每次操作都要移动很多元素
    • ArrayList是非线程安全的,很多操作非原子性的,例如elementData[size++] = e

    相关文章

      网友评论

        本文标题:集合源码解析之ArrayList

        本文链接:https://www.haomeiwen.com/subject/dgygjktx.html