• Java基础 ArrayList源码分析 JDK1.8


    一、概述

      本篇文章记录通过阅读JDK1.8 ArrayList源码,结合自身理解分析其实现原理。

      ArrayList容器类的使用频率十分频繁,它具有以下特性:

    • 其本质是一个数组,因此它是有序集合
    • 通过 get(int i) 下标获取数组的指定元素时,时间复杂度是O(1)
    • 通过 add(E e)插入元素时,可直接向当前数组最后一个位置插入(这个描述不是特别准确,其中涉及到扩容、后续将讲解),其时间复杂度为O(1)
    • 通过 add(int i, E e)向指定位置插入元素时,是在原数组的基础上通过拷贝和偏移量实现,其时间复杂度为O(n)
    • 通过 remove(int i) 删除指定位置的元素时,同样也是在原数组的基础上通过拷贝和偏移量实现,其时间复杂度为O(n)
    • 使用内部类Itr()迭代删除ArrayList删除元素时,需使用迭代器的remove()方法,不能使用ArrayList.remove(Object o)方法
    • ArrayList是非线程安全的,可能造成数据丢失,数组越界的问题

    二、源码分析

    1.重要属性

    private static final int DEFAULT_CAPACITY = 10;  //  扩容因子默认为10
    
    transient Object[] elementData;            // 元素数据
    
    private int size;                    //  当前ArrayList中实际元素数量
    
    protected transient int modCount = 0;        //  ArrayList被修改的次数,这个属性是从java.util.AbstractList继承下来的

       

    2.重要操作

      构造器

    /*  
     * 无参构造器 将elementData初始化为一个空的数组
    */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    /*
     * int整形构造器,将elementData初始化为指定大小的数组
    */
    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);
        }
     }

      boolean add(E e)  添加一个元素

     将添加数组分解成以下5个步骤

    //  添加一个元素时 必须确保当前数组可以添加一个新的元素,因此会根据capacity去计算
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);
        
        // ⑤ 将新元素添加到 扩容之后的数组size++坐标上,(注意是size++,先赋值再自增size)
        elementData[size++] = e;
        return true;
    }
    
    
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    
    // ① 计算容量
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        /* 如果初始化ArrayList时 是使用无参构造 new ArrayList()
    那么第一次向ArrayList添加元素时会将容量设为DEFAULT_CAPACITY 10个*/
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } // ② 确认是否扩容 private void ensureExplicitCapacity(int minCapacity) { // ArrayList被修改次数 + 1 modCount++; /*
    如果计算出来的容量 > elementData数组的长度时,那么会要扩容 因为elementData数组放不下新元素了;
    否则的话就不需要扩容
    */ if (minCapacity - elementData.length > 0) grow(minCapacity); } // ③ 扩容 private void grow(int minCapacity) { // 新数组容量 = 老数组容量 * 1.5 int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); /* 但是必须要要保证 新容量 > ①中【计算出来的容量】 ,否则依然放不下新元素 如果出现这种情况,那么使用①中【计算出的容量】最为新数组容量,保证能放下元素 */ if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // 数组元素过多 使用Integer.MAX_VALUE 作为容量 if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // ④ 通过拷贝的方式扩容 调用native层方法 elementData = Arrays.copyOf(elementData, newCapacity); }

      举个例子

    // 往ArrayList中插入11个元素
    List<Object> list = new ArrayList<>();
    for (int i = 0; i < 11; i++) {
        list.add(new Object());
    }

     上述代码很简单,步骤为:

    1. 调用无参构造器,初始化一个ArrayList
    2. 循环向该ArrayList插入11个元素

    以该步骤为例:

      List<Object> list = new ArrayList<>();  //   初始化elementData为一个空数组{} , elementData.length = 0

      i = 0 时

      list.add(new Object());

      这个操作会经历上面代码片段的①②③④⑤步骤

    1. 在走步骤①时使用默认容量 10
    2. 走步骤②时  判断10 > elementData.length  因此使用10作为最小容量请求扩容
    3. 走步骤③时 elementData.length 扩大1.5倍后依然为0 因此使用10作为新容量
    4. 通过步骤④native层拷贝方法进行数组拷贝
    5. 通过步骤⑤在新数组的size++ (即第个0个坐标上),并且size自增为1

      当 1 ≤ i ≤ 9时 

       list.add(new Object());

      在步骤①算计容量时,minCapacity依次计算出来为 1, 2, 3, 4, 5, 6, 7, 8, 9 都小于elementData.length(当前经历了第一次扩容 为10) ,因此不会考虑扩容,直接将新增元素放到指定的下标中

      ps: 此步骤modCount每次都会+1

      当 i = 9时, 数组被填充满

     

      当 i = 10 时

      list.add(new Object());

      此时就不能将第11个元素放到数组中了,需要第二次扩容

    1. 执行代码片段步骤①,算出minCapacity=11
    2. 步骤②判断出minCapacity(11) > elementData.length(10),因此需要扩容
    3. 步骤③,通过在elementData.length基础上扩大1.5倍 即 15, 同时大于 minCapacity, 因此使用15作为新容量
    4. 步骤④,以15作为新数组容量,拷贝原数组到新数组中
    5. 步骤⑤,将新的元素放到扩容之后新数组下标10的位置中,并且size自增

      

    可以看到,上述操作过程中list经历了两次扩容,因此在使用ArrayList的时候可以考虑使用有参构造器,确认ArrayList的大小,防止扩容发生,影响效率

        add(int i, E e)  向指定位置添加一个元素

    public void add(int index, E element) {
        //    检查下标是否越界
        rangeCheckForAdd(index);
    
        //    计算容量 考虑是否扩容
        ensureCapacityInternal(size + 1);
        
        //    通过拷贝数组的方式插入
        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));
    }

    可以看到向指定位置插入元素和add(E e)方法步骤差不多,只是拷贝方式做了改变

      1. 不发生扩容时插入过程

      2. 发生扩容时插入过程

        

        E remove(int index)  根据下标删除元素

    public E remove(int index) {
        //    检查索引是否越界
        rangeCheck(index);
    
        //    修改次数 +1    
        modCount++;
        //    取出需要删除的元素
        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中elementData仅有10个元素并且存满了,remove掉9个元素后,size大小是1,但是elementData.length依旧为10

       

        void trimToSize()  修整大小

    //    这个方法可以以当前elementData数组的size实际大小 重新拷贝出一个数组
    //    防止上面问题的发生
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

        E get(int index)  从指定索引获取元素

    //    获取指定元素
    public E get(int index) {
        rangeCheck(index);
    
        return elementData(index);
    }
    
    E elementData(int index) {
        return (E) elementData[index];
    }

        这个方法很简单,检查完索引范围,直接从elementData数组上去取对应位置的元素

        boolean contains(Object o)  查询ArrayList中是否包含某个元素

    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }
    
    
    //    遍历elementData数组 查询满足条件的下标
    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;
    }

    可以看到查询ArrayList中是否包含某个元素时,是通过遍历整个数组,依次查询,因此该方法时间复杂度为O(n)

        

        Iterator<E> iterator()  迭代

    public Iterator<E> iterator() {
        return new Itr();
    }
    
    //    内部类 迭代器
    private class Itr implements Iterator<E> {
        int cursor;       // 返回数据的游标
        int lastRet = -1; // index of last element returned; -1 if no such
        int expectedModCount = modCount;    //    迭代器期望的被修改的次数 与ArrayList.modCount相同,
    
        Itr() {}
    
        public boolean hasNext() {
            return cursor != size;
        }
    
        @SuppressWarnings("unchecked")
        public E next() {
            //    ①  expectedModCount != modCount 时抛出ConcurrentModificationException
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
    
        //    通过迭代器进行删除,每删除一个元素时expectedModCount会被重新赋值
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
    
            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
    
        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }
    
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

    前文中多次提到ArrayList的属性modCount,内部类Itr中提供的remove方法,在删除元素成功后,会将expectedModCount进行更新,因此,通过迭代器迭代ArrayList时,不能使用ArrayList.remove(Object o)方法,如果直接使用ArrayList.remove(Object o)进行删除(这个方法会对modCount减1),会导致迭代器进行下一次迭代时调用next()方法 检查到expectedModCount与modCount不同(上述代码①处),抛出ConcurrentModificationException,下面的代码示例将展示正确与错误在迭代中删除元素的操作

    //    正确操作
    List<Integer> list = new ArrayList<>();
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()) {
        iterator.next();    //    next()操作会更新游标cursor
        iterator.remove();
    }
    
    
    //    错误操作:将抛出ConcurrentModificationException
    List<Integer> list = new ArrayList<>();
    Iterator<Integer> iterator = list.iterator();
    while (iterator.hasNext()) {
        Integer next = iterator.next();
        list.remove(next);
    }

    3.线程安全问题(非线程安全)

    /*  size++非原子操作  elementData[size++] = e  等同于
      elementData[size] = e;
      size = size + 1  */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);
        elementData[size++] = e;
        return true;
    }

      ArrayList是非线程安全的,原因是在add操作时,由于size++是非原子操作,多个线程去对同一ArrayList执行add(E e)方法时,会出现下面两种情况

      1.情况1:当两个线程同时进入add()方法,读取到当前size为0,并都往0的下标存元素时,就会导致其中的一个值被覆盖掉的问题


      2.情况2:假设当前ArrayList为elementData[10],可存放10个元素,并且当前已经存了9个元素了,那么此时size=9

    情况1
    线程A 线程B
    add(e1) add(e2)
    读到size为0 读到size也为0
    elementData[0] = e1  
      elementData[0] = e2
    情况2
    线程A 线程B
    add(e1) add(e2)
    读到size为9 不需要扩容 读到size为9 不需要扩容
    elementData[9] = e1  

    size=size+1//size = 10

     
     

    elementData[10] = e2

    // 由于没有扩容导致数组越界

    ... 抛出异常了

     

     

  • 相关阅读:
    Towards Life-Long Autonomy of Mobile Robots Through Feature-Based Change Detection
    Magnetic field constraints and sequence-based matching for indoor pose graph SLAM
    3D Image-based Indoor Localization Joint With WiFi Positioning
    Online Probabilistic Change Detection in Feature-Based Maps
    Detectron2--(1)
    detectron2 + ubuntu + cpu
    出去上网-ubuntu-ss
    深度学习入门4
    IDM添加代理
    [转载]clover引导黑苹果icloud已达到账户数量限制解决方法
  • 原文地址:https://www.cnblogs.com/Sherry-XRJ/p/10922213.html
Copyright © 2020-2023  润新知