ArrayList源码解析--值得深读

ArrayList源码解析

基于jdk1.8

ArrayList的定义

类注释

  • 允许put null值,会自动扩容;
  • size isEmpty、get、set、add等方法时间复杂度是O(1);
  • 是非线程安全的,多线程情况下推荐使用CopyOnWriteArrayList或者Vector。
  • 增强for循环或使用迭代器过程中,如果数组大小被改变会抛出异常。
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

从源码中我们可以看出,ArrayList继承了AbstractList,实现了List接口,RandomAccess,Cloneable,Serializable接口。

在这里插入图片描述

实现List接口提供了元素的添加、删除。修改。遍历等功能。
实现RandomAccess接口,他是一个标志性接口,表明实现这个接口后,List集合支持随机访问。
实现Cloneable接口表明ArrayList可以被克隆
实现Serializable接口表示ArrayList支持序列化,能被传输。

ArrayList是一个动态数组,与普通数组相比他的容量可以动态的扩容。

ArrayList的有关属性

 private static final long serialVersionUID = 8683452581122892189L;

    /*默认容量大小是10*/
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 空数组
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 用来共享空数组实例
     * 我们把它和EMPTY_ELEMENTDATA数组区分出来,当添加第一个元素的时候知道需要扩增多少容量
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 保存ArrayList数据的数组
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * 集合包含元素的个数
     */
    private int size;

ArrayList的构造函数

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

    /**
     * 构造一个空数组,当增加第一个元素的时候才确定数组容量是10
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     
     *构造函数的参数是一个集合,如果集合为null,会抛空指针异常
     *
     */
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        //如果集合的大小不为0 就通过Arrays.copy方法,把Object数组的元素一一复制到elementData数组中
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            // 注意这是java的一个bug,c.toArray()方法返回值可能不是Object数组,这里转成Object类型
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
        //如果集合里没有元素就是一个空数组
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

上面的那个// c.toArray might (incorrectly) not return Object[] (see 6260652)一般情况下都不会触发这个bug ,这里演示一下是怎么出现的。

public void testConstructor() {
        List<String> arrayList = Arrays.asList("Hello");
        //toArray方法返回Object数组类型
        Object[] objects = arrayList.toArray();
        log.info(objects.getClass().getSimpleName());
        // 打印出来是 String[]

        //这样写是对的
        //objects[0]="Java";
        
        //这样就会报错因为数组元素的类型是String
        objects[0] = new Object();
    }

不过这个bug已经在Java9解决。

新增和扩容的实现

新增元素


/**
     * Appends the specified element to the end of this list.
     *增加元素到集合的末尾
     * 
     */
    public boolean add(E e) {
           //先扩容,使容量+1
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    /**
     * 在指定位置增加元素
     *
     * @param index index at which the specified element is to be inserted
     * @param element element to be inserted
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public void add(int index, E element) {
       //检查增加的位置是否越界
       rangeCheckForAdd(index);
        //扩容,size+1
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //元素的复制,将 index之后的元素往后 移动一位,size++;
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
                         //最后在index位置上赋值
        elementData[index] = element;
        size++;
    }

扩容


 private void ensureCapacityInternal(int minCapacity) {
        //确保容积足够
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //如果出事容量大小有给定的值就一指定的值为准
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }


    private void ensureExplicitCapacity(int minCapacity) {
       //记录数组被修改的次数加1 
       modCount++;

        // 如果我们期望的数组大小大于目前的数组长度,那么就扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

    //扩容方法
     private void grow(int minCapacity) {
        // 先记录旧的数组大小
        int oldCapacity = elementData.length;
        //扩容后的容量=扩容前的+扩容前的大小/2,也就是说扩容后的值是原来的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果扩容后的值,小于期望的大小,那扩容后的值就改为期望值
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        
        //如果扩容后的值大于jvm所能分配的最大值,那么就用Integer.MAX_VALUE.,否则等于MAX_ARRAY_SIZE
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 最后数组复制,底层是System.arraycopy()方法
        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;
    }

  • 扩容后数组的容量是原先数组容量的1.5倍。
  • ArrayList中的数组长度最大是Integer.MAX_VALUE,超过这个值,JVM就不会给数组分配内存空间了。
  • 新增时,并没有对新增元素进行严格校验,所以可以新增null。

其实我们可以看出,大多数方法中都用到了Arrays.copyOf方法

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        //新建数组并返回数组
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(
        original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

再看System.arraycopy

//src 是原数组,srcPos是原数组的位置,dest是目标数组,destPOS是目标数组的位置,length是拷贝的长度,这个方法是native方法,所以底层可能是C/C++编写的
public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

删除和迭代

ArrayList支持两种删除元素的方式

  1. remove(int index) 按照下标删除
  2. remove(Object o) 按照元素删除,删除第一个匹配到的元素。
public E remove(int index) {
        //数组下标越界检查
        rangeCheck(index);
        //修改次数自增1
        modCount++;
        //记录被删除的值
        E oldValue = elementData(index);
       // 记录将要从index后移动的多少个位数到前面,因为size是从0开始,index是从1开始所以需要减一
        int numMoved = size - index - 1;
        if (numMoved > 0)
            //从elementData的index+1处拷贝,拷贝到elementData数组中,从index处开始放置拷过来的元素,拷贝的长度是 numMoved
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // 最后一个元素赋值为null,帮助GC

        return oldValue;
    }

    /**
     根据值删除元素
     */
    public boolean remove(Object o) {
    //y因为ArrayList允许元素值为null,所以删除的时候遍历一次找到第一个是null的值,然后移除。
        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++)
            //这里是根据equals方法判断值是否相等,相等再根据索引删除
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
    //记录修改数加1
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; //最后元素赋值成null,有助于GC
    }

    /**
        移除集合中所有元素
     */
    public void clear() {
        modCount++;

        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            //将集合中的每一个元素赋值为null
            elementData[i] = null;
        //集合元素的个数设置成0
        size = 0;
    }

  • 新增时因为没有对null做校验所以可以新增null,删除的时候就需要对null值做判断了,按照值删除元素实际上还是根据值找到这个元素的索引,进行删除。比较的方法是通过equals方法。
  • 某一个元素被删除后,ArrayList采用的是直接将这个元素后面的所有元素复制到原数组中,最后的一个元素的值设置成null,让JVM进行GC操作。

接下来看迭代


public ListIterator<E> listIterator(int index) {
    //如果索引越界直接抛异常IndexOutOfBoundsException,否则返回从指定下标开始的所有元素列表(按熟顺序),ListIterator接口继承了Iterator接口
        if (index < 0 || index > size)
            throw new IndexOutOfBoundsException("Index: "+index);
        return new ListItr(index);
    }

    /**
     * 返回列表中的列表迭代器按顺序返回,迭代器是fail-fast
     */
    public ListIterator<E> listIterator() {
        return new ListItr(0);
    }

    /**
     * 以正确顺序返回元素列表的迭代器
     */
    public Iterator<E> iterator() {
        return new Itr();
    }

//ltr是ArrayList的内部类,实现了 Iterator接口,这里只是Itr的部分代码
 private class Itr implements Iterator<E> {
        int cursor;       // i迭代过程中,下一个元素的位置,默认从0开始
        int lastRet = -1; // 新增时表示上次迭代过程中,索引的位置;删除场景下是-1
        int expectedModCount = modCount;
        //期望修改的版本号=实际的修改版本号
        Itr() {}

        public boolean hasNext() {
        //判断下一个元素存不存在,只需知道下一个索引的下标是不是数组大小,如果不是返回true,如果是返回false
            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() {
        //当lastRet小于0,也就是说在多个线程操作时,直接将迭代器中的列表删完了,里面没有元素了,就会抛出IllegalStateException
            if (lastRet < 0)
                throw new IllegalStateException();
           //迭代过程中判断版本号有没有被修改,如果被修改抛出ConcurrentModificationException
           checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                //lastRet=-1防止重复删除
                lastRet = -1;
                //下次迭代时期望的版本号和实际的版本号一致
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }
//版本号比较
final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }

小结

  • ArrayList底层是使用类似于动态数组实现的,默认构造方法初始化的容量是10,但是在没有指定容量大小时区add第一个元素时才会确定出数组的容量。
  • 扩容时,扩容后的长度是扩容前的1.5倍
  • 实现了RandomAccess接口,支持随机读写,get读取元素的性能较好
  • 线程时不安全的,是因为自身的elementData、size、modCount在进行各种操作时,都没有加锁
  • 新增(这里的新增默认是增加到尾部)和删除方法 的时间复杂度是O(1)

以上理解有误的地方欢迎指出,共同进步!

posted on 2020-05-07 16:52  青山是谁  阅读(17)  评论(0编辑  收藏  举报