• jdk 8 ArrayList源码


    前言

    ArrayList可以说是日常开发中最经常使用到的一个集合了,下面来分析一下它的结构和几个常用方法。

    ArrayList的继承关系如下所示:

    需要注意的ArrayList实现了RandomAccess这个接口,其源码如下:

    public interface RandomAccess {
    }
    

    可以看到这个接口里并没有任何属性或者方法,那为什么还有实现呢?主要是为了做一个标记,用来表明实现该接口的类支持快速(通常是固定时间)随机访问。此接口的主要目的是允许一般的算法更改其行为,从而在将其应用到随机或连续访问列表时能提供良好的性能

    整体结构

    ArrayList底层其实是一个数组,比较简单,如下所示:

    ArrayList的特点如下:

    • 允许null值;
    • 有序,可根据索引快速访问;
    • 非线程安全;
    • 快速失败,若是在遍历过程中,对集合进行结构性修改(增,删),会导致快速失败(fast-fial),关于fast-failfast-safe相关知识可以这里

    成员变量

    //默认容量
     private static final int DEFAULT_CAPACITY = 10;
     //空数组 
      private static final Object[] EMPTY_ELEMENTDATA = {};
      //默认空数组 无参构造时用到
     private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
      //元素数组 真正用于存储元素
     transient Object[] elementData;
    
    //数组最大长度
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
     //元素个数
      private int size;
    

    构造方法

    //无参构造 
    public ArrayList() {
            // 默认使用DEFAULTCAPACITY_EMPTY_ELEMENTDATA 此时大小为0
            this.elementData = DEFAULTDEFAULTCAPACITY_EMPTY_ELEMENTDATACAPACITY_EMPTY_ELEMENTDATA;
        }
    
    //带有 capacity入参的构造方法
     public ArrayList(int initialCapacity) {
            if (initialCapacity > 0) {
                this.elementData = new Object[initialCapacity];
            } else if (initialCapacity == 0) {
                //等于0是 使用EMPTY_ELEMENTDATA 此时大小为0
                this.elementData = EMPTY_ELEMENTDATA;
            } else {
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            }
        }
        //集合作为入参的构造方法
       public ArrayList(Collection<? extends E> c) {
            //elementData 是保存数组的容器,默认为 null
            elementData = c.toArray();
           //如果给定的集合(c)数据有值
            if ((size = elementData.length) != 0) {
                //如果集合元素类型不是 Object 类型,我们会转成 Object
                if (elementData.getClass() != Object[].class)
                    elementData = Arrays.copyOf(elementData, size, Object[].class);
            } else {
                // 给定集合(c)无值,则默认空数组
                this.elementData = EMPTY_ELEMENTDATA;
            }
        }
    

    关于这几个构造函数需要注意的点:

    • ArrayList无参构造初始化时,默认大小数组是空数组,而不是常说的10ArrayList也是懒加载,只有真正存放数据时才会去进行扩容;
    • 在使用集合作为入参的构造函数时,,当给定集合内的元素不是Object类型时,默认会将其转换为Object类型

    常用方法

    add(E e)

     public boolean add(E e) {
         //检测目前的数组是否还能继续存放元素,不够执行扩容,size 为当前数组的大小
            ensureCapacityInternal(size + 1);  
           //直接赋值,线程不安全的
            elementData[size++] = e;
            return true;
        }
    
     private void ensureCapacityInternal(int minCapacity) {
             // 调用无参构造时进入该if分支
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                //这也说明了,当使用无参构造时,第一次add时,数组会长度会由0增加到10
                minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
            }
            //确保容积足够
            ensureExplicitCapacity(minCapacity);
        }
    
    private void ensureExplicitCapacity(int minCapacity) {
            // 更新modCount
            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;
            //如果新数组长度大于最大允许的数组长度 新数组容量设为Integer的最大值
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // 通过复制进行扩容
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    
    //private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
    private static int hugeCapacity(int minCapacity) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return (minCapacity > MAX_ARRAY_SIZE) ?
                Integer.MAX_VALUE :
                MAX_ARRAY_SIZE;
        }
    

    add(E e)的逻辑还是比较清晰和简单的:

    • 先判断是否需要扩容,需要的话就进行扩容,否则直接赋值即可;
    • 扩容时,新数组的长度为旧数组长度的1.5倍
    • 扩容时调用的Arrays#copyOf方法其底层最后调用的是一个native方法,System#arraycopy
    • ArrayList中的数组的最大值是Integer.MAX_VALUE,超过该值时JVM不会再继续分配内存空间了;
    • 新增时,没有对入参做校验,因而ArrayList允许为null

    add(int index, 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
            size++;
        }
    
    private void rangeCheckForAdd(int index) {
            if (index > size || index < 0)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
    

    没啥好分析的,看一下注释就行。

    add(Collection<? extends E> c)

     public boolean addAll(Collection<? extends E> c) {
            //转为Object数组
            Object[] a = c.toArray();
            //获取集合长度
            int numNew = a.length;
            // 检查是否需要进行扩容
            ensureCapacityInternal(size + numNew);
            //将c添加到数组中
            System.arraycopy(a, 0, elementData, size, numNew);
           //更新size
            size += numNew;
            return numNew != 0;
        }
    

    get(int index)

    public E get(int index) {
            //检测下标是否越界
            rangeCheck(index);
            //直接返回索引位置处的数据
            return elementData(index);
        }
    

    remove(int index)

    public E remove(int index) {
        //检测下标是否越界
            rangeCheck(index);
        // 更新modCount
            modCount++;
           //索引位置处的值
            E oldValue = elementData(index);
             // numMoved 表示删除 index 位置的元素后,需要从 index 后移动多少个元素到前面去
          // 减 1 的原因,是因为 size 从 1 开始算起,index 从 0开始算起
            int numMoved = size - index - 1;
            if (numMoved > 0)
                 // 从 index +1 位置开始被拷贝,拷贝的起始位置是 index,长度是 numMoved
                System.arraycopy(elementData, index+1, elementData, index,
                                 numMoved);
            //置为null 帮助gc
            elementData[--size] = null; // clear to let GC do its work
        
            // 返回旧值
            return oldValue;
        }
    

    remove(Object o)

     public boolean remove(Object o) {
             // 如果o为null
            if (o == null) {
                 // 正序遍历,将第一个null对象移除
                for (int index = 0; index < size; index++)
                    if (elementData[index] == null) {
                        fastRemove(index);
                        return true;
                    }
            } else {
                // 否则的话遍历数组进行查找,然后删除
                for (int index = 0; index < size; index++)
                    // 这里是根据  equals 来判断值相等的,相等后再根据索引位置进行删除
                    if (o.equals(elementData[index])) {
                        fastRemove(index);
                        return true;
                    }
            }
            return false;
        }
    
    // 可以看出没有返回值的 remove(int index)
     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
        }
    

    trimToSize()

      public void trimToSize() {
          // 更新modCount
            modCount++;
            if (size < elementData.length) {
                elementData = (size == 0)
                  ? EMPTY_ELEMENTDATA
                  : Arrays.copyOf(elementData, size);
            }
        }
    

    ArrayList中,一般情况下数组长度elementData.length都是大于等于数组中实际元素个数的size的,所以可以通过trimToSize方法将这个数组转变为数组长度和元素个数一样的数组,减少内存的占用,如下所示:

    set(int index,E e)

     public E set(int index, E element) {
            //下标校验
            rangeCheck(index);
            //获取旧值
            E oldValue = elementData(index);
            //直接设置
            elementData[index] = element;
            return oldValue;
        }
    

    subList(int fromIndex,int toIndex)

    /**
     * 返回集合中指定 [fromIndex, toIndex) 位置元素构成的集合
     * 如果 fromIndex == toIndex,返回空集合
     */
    public List<E> subList(int fromIndex, int toIndex) {
            //下标校验
            subListRangeCheck(fromIndex, toIndex, size);
            //返回SubList
            return new SubList(this, 0, fromIndex, toIndex);
        }
    
    /*
     * 检测子集的下标是否越界
     */
       static void subListRangeCheck(int fromIndex, int toIndex, int size) {
            if (fromIndex < 0)
                throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
            if (toIndex > size)
                throw new IndexOutOfBoundsException("toIndex = " + toIndex);
            if (fromIndex > toIndex)
                throw new IllegalArgumentException("fromIndex(" + fromIndex +
                                                   ") > toIndex(" + toIndex + ")");
        }
    
    private class SubList extends AbstractList<E> implements RandomAccess {
            private final AbstractList<E> parent;
            private final int parentOffset;
            private final int offset;
            int size;
    
            SubList(AbstractList<E> parent,
                    int offset, int fromIndex, int toIndex) {
                this.parent = parent;
                this.parentOffset = fromIndex;
                this.offset = offset + fromIndex;
                this.size = toIndex - fromIndex;
                this.modCount = ArrayList.this.modCount;
            }
         ..........
        
    }
    

    返回集合中指定的 [fromIndex, toIndex) 位置之间的集合。 如果 fromIndex == toIndex,则返回集合为空。

    iterator() && listIterator()

    /**
     * 以正确的顺序返回此集合中的所有元素的迭代器
     * 返回的迭代器为 fast-fail
     */ 
    public Iterator<E> iterator() {
            return new Itr();
        }
    /**
     * 返回此集合中的所有元素的 list 迭代器
     * 返回的 list 迭代器为 fast-fail
     */
    public ListIterator<E> listIterator() {
            return new ListItr(0);
        }
    

    通过这两个方法可以获取一个迭代器用于遍历集合。

    迭代器 Iterator

    在使用ArrayList时我们经常需要遍历集合来完成某些操作,通常有两种方式,一种是直接使用foreach,一种是使用Iterator接口,但两种本质上是一样的,foreach底层实现还是Iterator接口。

    Itr

    ItrArryList中的一个私有内部类,实现了Iterator接口用以实现遍历。

    成员变量

       int cursor;       // 迭代过程中,下一个元素的位置,默认从 0 开始
       int lastRet = -1; // 新增场景:表示上一次迭代过程中,索引的位置;删除时置为 -1
       int expectedModCount = modCount;// expectedModCount 表示迭代过程中,期望的版本号;
                                            //modCount 表示数组实际的版本号
    

    主要方法

    迭代器主要就三个方法:

    • hasNext:是否还有下一个元素;
    • next:下一个元素值;
    • remove:删除当前迭代的值;

    hasNext()

      public boolean hasNext() {
          //cursor 表示下一个元素的位置,size 表示实际大小,如果两者相等,说明已经没有元素可以迭代了,如果
          //不等,说明还可以迭代
                return cursor != size;
            }
    

    next()

     @SuppressWarnings("unchecked")
            public E next() {
               //迭代过程中,判断版本号有无被修改,有被修改,抛 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;
                // 返回元素值 同时将lastRet设置为 i
                return (E) elementData[lastRet = i];
            }
    
            //检测 ArrayList 中的 modCount 和当前迭代器对象的 expectedModCount 是否一致
            // 不等的话直接抛出异常
            final void checkForComodification() {
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
            }
    

    next方法做了两件事:

    • 是否还能继续迭代;
    • 定位到当前迭代的值,并为下一次迭代做好准备;

    remove()

            public void remove() {
                 // 如果上一次操作时,数组的位置已经小于 0 了,说明数组已经被删除完了
                if (lastRet < 0)
                    throw new IllegalStateException();
                 //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
                checkForComodification();
    
                try {
                    ArrayList.this.remove(lastRet);
                    cursor = lastRet;
                    // -1 表示元素已经被删除,这里也防止重复删除
                    lastRet = -1;
                 // 删除元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount
                  // 这样下次迭代时,两者的值是一致的了
                    expectedModCount = modCount;
                } catch (IndexOutOfBoundsException ex) {
                    throw new ConcurrentModificationException();
                }
            }
    

    需要注意的点是:

    • lastRet = -1 的操作目的,是防止重复删除操作;
    • 删除元素成功,数组当前 modCount 就会发生变化,这里会把 expectedModCount 更新为modCount的值,下次迭代时两者的值就会一致了;

    ListItr

    ListItrItr的子类,除了基本的三个方法之外,还额外实现了一些其他的方法。我们都知道,在遍历集合时,若是对集合结构进行修改则会触发fast-fail机制,但若是我们的确有这个需求,一边遍历一边修改集合的结构,那怎么办呢?ListItr中提供了add这个方法。

    add(E e)

            public void add(E e) {
                //迭代过程中,判断版本号有无被修改,有被修改,抛 ConcurrentModificationException 异常
                checkForComodification();
    
                try {
                    //获取当前遍历到的位置
                    int i = cursor;
                    //插入到数组中
                    ArrayList.this.add(i, e);
                    //再向下移动一个位置 因而本次遍历过程中访问不到这个元素 只能在下一次遍历中才能访问
                    cursor = i + 1;
                    //更新lastRet
                    lastRet = -1;
                  // 添加元素时 modCount 的值已经发生变化,在此赋值给 expectedModCount
                  // 这样下次迭代时,两者的值是一致的了
                    expectedModCount = modCount;
                } catch (IndexOutOfBoundsException ex) {
                    throw new ConcurrentModificationException();
                }
            }
        }
    

    使用add这个方法在遍历时添加时,本次遍历是访问不到添加的那个元素的,只能在下一次遍历时才能访问,保证了本次遍历的正确性,也防止出现快速失败。

    其他方法

    ListItr还实现了ListIterator这个接口,因而还可以向前遍历,这里只给出源码,也很简单,看一下就行。

           //判断是否还有前一个元素
            public boolean hasPrevious() {
                return cursor != 0;
            }
            //下个元素的索引
            public int nextIndex() {
                return cursor;
            }
    
           //前一个元素的索引
            public int previousIndex() {
                return cursor - 1;
            }
    
          //迭代时,前一个元素的值
            @SuppressWarnings("unchecked")
            public E previous() {
                //校验modCount
                checkForComodification();
                //前一个位置索引
                int i = cursor - 1;
                if (i < 0)//参数校验
                    throw new NoSuchElementException();
                //实际存储数据的数组
                Object[] elementData = ArrayList.this.elementData;
                if (i >= elementData.length)//参数校验
                    throw new ConcurrentModificationException();
                //更新cursor
                cursor = i;
                //返回当前迭代的值,并设置 lastRet
                return (E) elementData[lastRet = i];
            }
    

    总结

    ArrayList的特点总结如下:

    • 底层是动态数组,默认大小为10,扩容时每次都是当前数组长度的1.5倍;

    • 使用无参构造或者capacity=0的有参构造时,ELEMENTDATA都是空数组,只有在第一次添加元素时才去扩容;

    • 扩容主要方法为 grow(int minCapacity)不支持缩容

    • 允许元素为null

    • 实现 RandomAccess 接口,支持随机访问,平均时间复杂度为 O(1)

    • 增加和删除元素过程中,效率低下,平均时间复杂度为 O(n)

  • 相关阅读:
    Git安装(操作篇)
    Git安装
    ES6基础练习
    SVN的安装与搭建及使用
    解决SVN文件不显示绿色小钩图标问题
    混入(mixin)
    ref属性与props配置项
    docker-compose部署 Mysql 8.0 主从模式基于GTID
    项目统一处理
    Docker Compose实战
  • 原文地址:https://www.cnblogs.com/reecelin/p/13463569.html
Copyright © 2020-2023  润新知