• JDK源码分析(一)——ArrayList


    ArrayList分析

      JDK api对ArrayList是这样介绍的:

    Resizable-array implementation of the List interface. Implements all optional list operations, and permits all elements, including null. In addition to implementing the List interface, this class provides methods to manipulate the size of the array that is used internally to store the list. (This class is roughly equivalent to Vector, except that it is unsynchronized.)

    大意是ArrayList是List接口的可变长度数组形式的实现,允许插入包括Null在内的所有元素,大致相当于Vector类,但ArrayList类不是线程安全的。

    ArrayList继承结构

    ArrayList.png

      ArrayList是List的数组形式实现,所以实现了List接口,而List又从属于整个大的Colletction集合内,所以Collection是ArrayList的父接口,ArrayList实现了Iterable接口,可以使用迭代器和for-each循环对ArrayList对象进行遍历。Cloneable接口,允许对对象进行拷贝,Serializable接口,序列化相关接口,RandomAccess是一个标记接口,用于标明实现该接口的List支持快速随机访问,主要目的是使算法能够在随机和顺序访问的list中表现的更加高效。AbstractList和AbstractCollection提供了List接口和Collection接口的骨干实现。

    ArrayList字段属性

     //序列化ID
     private static final long serialVersionUID = 8683452581122892189L; 
     //初始化ArrayList对象时Object数组默认的长度,是不可变常量
     private static final int DEFAULT_CAPACITY = 10;
     //空数组
     private static final Object[] EMPTY_ELEMENTDATA = {};
     //使用无参的构造方法时,Object数组会用这个常量初始化
     private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
     //ArrayList存储数据的底层数组,transient修饰表示字段不会被序列化
     transient Object[] elementData; 
     //存储元素的个数
     private int size;
    

    ArrayList构造函数

    无参构造函数

      在使用无参构造器初始化ArrayList对象的时候,底层的Object数组用DEFAULTCAPACITY_EMPTY_ELEMENTDATA常量数组来初始化,可以看到这是一个空数组,JDK的注释 Constructs an empty list with an initial capacity of ten,但实际上初始化的是一个空数组,可能早期的会初始一个容量为10的数组。

    	public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        }
    

    有参构造函数

      这是带有int整形参数的构造器,参数的作用也很明显,作为底层的Object数组的初始容量。initialCapacity参数大于0,初始化一个容量为initialCapacity的Object数组;initialCapacity等于0,则初始化为空数组;小于0,抛出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对象,包含传入集合对象的元素,方法的实现先把集合对象c转换为数组,用这个数组来初始化Object数组elementData,到这里为什么似乎size = elementData.length再判断一下size是否为0就结束了,然而程序里还要判断这个elementData数组的类型是不是Object数组,这是为什么呢?这是因为toArray这个方法由于实现方式不一样,可能返回的不是Object数组,而可能是String数组等等,那么elementData就会向下转型,它就不再是Object数组了,如果不是Object类型,使用Arrays.copy()复制,并指定类型为Object。

    	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;
            }
        }
    

    重要方法

    add(E e)

      使用add方法插入一个对象,首先得确保数组还能放一个对象,所以先执行ensureCapacityInternal(size + 1),看看size+1有没有出边界,如果大于elementDate数组长度,那就需要对数组进行扩容,否则就会超出边界报错了,当minCapacity - elementData.length > 0是就会执行grow方法扩容,扩容语句int newCapacity = oldCapacity + (oldCapacity >> 1);可以看到新的容量是以前的容量的1.5倍(>>1 右移一位,除以2),然后再把以前数组的元素拷贝到新数组中。

    	public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
    
    	private void ensureCapacityInternal(int minCapacity) {
            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);
        }
    
    	private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            // minCapacity is usually close to size, so this is a win:
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    

    add(int index, E element)

      add(int index, E element)方法带有两个参数,第一个参数是要插入的位置索引,第二个是要插入的元素,并且方法是无返回值的。
      由于add(int index, E element)是插入指定位置的,所以要先检查索引index,插入之前执行rangeCheckForAdd(index);这个方法也很简单,就是判断如果index大于ArrayList的size(注意不是Object数组容量而是实际元素个数)或index小于0,抛出IndexOutOfBoundsException异常,否则说明index位置处可以插入,当然还是要确保还有空间插入对象,执行ensureCapacityInternal(size + 1)方法,使用System.arrayCopy()将从index-size这段元素复制到elementDate数组,从index+1处开始接收,实际上就是把elementData数组从index-size这段元素依次向后移一位,那么index处就可以插入新元素了。计算复制元素的长度,最后一个元素索引是size-1,从index处元素开始后移,及index-1后面的元素都要后移,所以length=size-1-(index-1)=size-index
      这个插入并不是把index处元素覆盖了,它相当于一个“插队”的效果,把原来index及后面的元素挤到后面去了。

    	public void add(int index, E element) {
            rangeCheckForAdd(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));
        }
    

    get(int index)

      get方法,根据传入的index参数获取对应位置的值。首先检查索引是否有效,返回索引位置的元素。可以看出,由于ArrayList底层是基于数组实现的,所以根据索引查找较方便,但是插入,尤其是在中间位置的插入就比较麻烦了,需要把后面的元素依次后移,这是由数组的特性决定的。

    	public E get(int index) {
            rangeCheck(index);
    
            return elementData(index);
        }
    
    	private void rangeCheck(int index) {
            if (index >= size)
                throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
    

    set(int index, E element)

      根据传入index参数设置索引位置的值,并返回之前的元素,实现的是一种“更新”的操作。 先检查索引,索引有效,保存先前的值,再把新传入的元素的值设置进去,再返回先前的值。

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

    remove(int index)

      根据传入参数index移除指定位置的元素,并返回移除的元素。
      检查索引,索引有效,先保留index处的值用于返回,我们要移除index的值,那么就相当于index处空了,后面的元素需要一次向前移,实现这个依次前移,依然要使用System.arrayCopy方法,把自己本身的部分元素复制到自己的另一处位置,实际上是把index+1-size这一段元素复制到index-(size-1)处,实现依次前移的效果。我们前移是使用复制实现的,前面的元素被后一个的元素覆盖了,最后一个元素elementData(size-1)没有被覆盖,需要手动置为null。 最后一个元素的索引是size-1,那 numMoved=size-1-index,注意索引是从0开始的,而size表示的当前存储的元素个数,从1开始的
      这里的modCount属性统计list修改次数,每次执行add或remove方法modCount加一。这个字段在迭代器遍历是检查有没有其他线程对ArrayList进行修改是用到,因为我们知道ArrayList没有同步方法。

    	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);
            elementData[--size] = null; // clear to let GC do its work
    
            return oldValue;
        }
    

    remove(Object o)

      根据传入对象移除,首先循环遍历去找这个元素,按照传入参数是null还是非null,如果找到了,记下这个下标,调用fastRemove方法,根据这个下标移除。

    	public boolean remove(Object o) {
            if (o == null) {
                for (int index = 0; index < size; index++)
                    if (elementData[index] == null) {
                        fastRemove(index);
                        return true;
                    }
            } else {
                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);
            elementData[--size] = null; // clear to let GC do its work
        }
    

    clear( )

      把ArrayList存储的元素全部清除,注意并不是把elementData数组赋值为空数组,而是把数组每个元素置为空,方便GC工作。

    	public void clear() {
            modCount++;
    
            // clear to let GC do its work
            for (int i = 0; i < size; i++)
                elementData[i] = null;
    
            size = 0;
        }
    

    trimToSize()

      把elementData数组的容量压缩到和实际存储元素个数一致,我们知道底层的Object数组是有一个最大容量的,假如容量为10,但是我们只存放了3个元素,调用trimToSize就会数组压缩为一个容量(数组length)为3的新数组。 先判断size是否小于数组的length,小于的话,如果size为0,直接把空数组赋值给elementData;size不为0,复制有数据的那一部分给elementData。

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

    ArrayList Iterator迭代器

       ArrayList通过内部类Itr实现了Iterator接口,我么在使用迭代器时,通过ArrayList的成员方法 Iterator方法返回一个迭代器对象,就可以使用迭代器了,分析一下这个内部类。

    内部类字段属性

      内部类Itr实现了Iterator接口,Itr没有提供构造方法,编译器回味Itr类提供无参构造方法,供外部的iterator方法调用。
      cursor,表示下一个要迭代的元素的下标索引,初始默认为0;lastRet,代表当前元素的上一个元素的下标索引,初始为-1;expectedModCount,表示在迭代时ArrayList更改的次数,通过比较expectedModCount与实际的modCount是否一致来确定是否有其他线程修改了ArrayList,类似于乐观锁的CAS机制。

    	public Iterator<E> iterator() {
            return new Itr();
        }	
    
    	private class Itr implements Iterator<E> {
            int cursor;       // index of next element to return
            int lastRet = -1; // index of last element returned; -1 if no such
            int expectedModCount = modCount;
    
    		public boolean hasNext() {
                return cursor != size;
            }
    
            @SuppressWarnings("unchecked")
            public E next() {
                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];
            }
    
            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();
                }
            }
    
    		final void checkForComodification() {
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
            }
    	}
    

    hasNext()

      一般使用迭代器遍历时,都会使用hasNext作为while循环遍历条件,hasNext方法在ArrayList中还有元素没有遍历完就返回true,它会比较当前迭代元素下标cursor和ArrayList的size,不相等说明还没有遍历完。hasNext方法要配合next()方法一起使用,Next()方法会返回cursor指向的元素并把cursor加1。

    next()

      next()返回cursor指向的元素,并把cursor加1。
      执行next方法,首先执行checkForComodification方法,检查expectedModCount与实际的modCount是不是相等,就是检查有没有其他线程来使用了这个ArrayList(具体来说是执行了add或remove修改方法),检查通过,再检测cursor是否有效,是否超出了size,最后将cursor加1,i赋值给lastRet,实际上相当于lastRet++,再将索引为i处的元素返回。

    remove( )

      我们在平时的使用知道,remove方法要在next方法后使用的,否则要报错,从remove的源码我们可以看到移除是根据lastRet参数,首先检查lastRet是否小于0(lastRet初始化-1,执行remove也会赋值为-1),确定lastRet大于等于0,使用remove(int index)方法移除元素,此时将游标cursor指向lastRet,lastRet赋值为-1。执行完remove方法,lastRet为-1,此时再执行remove肯定会在if (lastRet < 0)抛出异常,必须通过next方法让lastRet指向恢复正常。

    总结

      ArrayList作为我们实际开发中常用的集合类,它存储元素是基于底层的Object数组,ArrayList实现了动态扩容,每次插入元素会检查数组容量是否足够,每次扩容会扩大至原有容量的1.5倍,由于数组特性,ArrayList根据索引查找元素较快,增加元素,尤其是在中间插入删除元素较慢,因为都会涉及到数组的赋值,JDK为了提高效率复制方法使用System.arraycopy()是原生native方法,提高效率。另外从源码也可以看出,ArrayList是线程不安全的,在使用迭代器迭代时,如果有其他线程改变了modCount,就会抛出ConcurrentModificationException异常,涉及到多线程环境就不宜使用ArrayList。

    编程改变世界
  • 相关阅读:
    redis 使用 get 命令读取 bitmap 类型的数据
    如何用vue-router为每个路由配置各自的title
    vue定义全局变量和全局方法
    后端分布式系列:分布式存储-HDFS 与 GFS 的设计差异
    Android Studio(十二):打包多个发布渠道的apk文件
    Android中使用lambda表达式
    Android教程-03 常见布局的总结
    程序员的业余项目
    Android Studio(十一):代码混淆及打包apk
    win10提示 磁盘包含不是“PARTITION_BASIC_DATA_GUID"类型的分区
  • 原文地址:https://www.cnblogs.com/rain4j/p/9277527.html
Copyright © 2020-2023  润新知