ArrayList源码分析(jdk 1.8)

写在前面

ArrayList相信大家做开发的同学都不陌生,在开发过程中这应该是最常用的数据结构了吧。但是现在是“源码时代”,会用还不够,要知道他的实现原理,本文主要基于jdk1.8对ArrayList源码进行分析。

 

一、从主要字段开始

值得注意的是,ArrayList内部会有一个modCount字段,但是这个字段是在父类AbstractList中的,代表着修改次数,后面会讲

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

    /**
     * 空元素集,构造函数传入空集合的时候使用
    */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 空元素集,默认无参构造函数中使用
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
        ArrayList真正保存元素的数组,在第一次插入元素的时候扩容
     * 
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * 当前数组元素个数
     *
     * @serial
     */
    private int size;

接下来看一下主要的构造方法:

/**
     * Constructs an empty list with the specified initial capacity.
     *
     * @param  initialCapacity  the initial capacity of the list
     * @throws IllegalArgumentException if the specified initial capacity
     *         is negative
     */
    //这个是带数组容量的构造函数
    public ArrayList(int initialCapacity) {
        //如果初始容量大于0,则初始化底层元素数组
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //如果等于0,则将上面的空数组赋值
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //如果是小于0的负数,抛非法参数异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        //默认无参构造给的是一个空数组,不在这个时候扩容
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * Constructs a list containing the elements of the specified
     * collection, in the order they are returned by the collection's
     * iterator.
     *
     * @param c the collection whose elements are to be placed into this list
     * @throws NullPointerException if the specified collection is null
     */
    //传入集合的构造函数
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        //如果长度不为0,进此逻辑
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            //如果该元素数组类型不是等于Object[],则拷贝一份Object[]类型的数组 并赋值给
            //elementData
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            //长度是0,给空数组
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }

二、add方法

1、list默认的add方法:

public boolean add(E e)

/**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        //好这边可以看到,在添加元素前搞了一个这个不可描述的动作,从之前的构造方法我们可以看出,
        //一开始数组根本没有初始化,所以扩容动作必定在这个函数里做的
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //这里添加元素
        elementData[size++] = e;
        return true;
    }

来看一下那个函数

private void ensureCapacityInternal(int minCapacity) {
        //调用了这个函数,记住这个minCapacity代表啥:size+1
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

再来看一下参数里面这个calculateCapacity函数

private static int calculateCapacity(Object[] elementData, int minCapacity) {
        //如果元素数组为空数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //返回默认容量和size+1中较大的那个
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        //如果不是空数组,返回size+1
        return minCapacity;
    }

继续跟踪:

private void ensureExplicitCapacity(int minCapacity) {
        //首先modCount加1,这个是干嘛用的后面会说
        modCount++;

        // overflow-conscious code
        //关键点来了,这句话啥意思?size+1比当前数组容量大的时候,很明显刚开始是0,1比0大很正常
        if (minCapacity - elementData.length > 0)
            //grow方法明显是扩容的真正方法
            grow(minCapacity);
    }

继续来看grow函数:

/**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        //获取数组原始容量
        int oldCapacity = elementData.length;
        //注意这行,扩容后的新容量等于老容量右移一位加上自身,也就是原来的1.5倍,而不是hashmap的        
          //两倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果新容量还是比size+1小,这是什么情况?想想
        //当然是老容量为0的时候,0*1.5=0,所以这个时候新容量等于1
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //当扩容之后的新容量比这个整形最大值-8还要大的时候
        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);
    }

来看一下上面的hugeCapacity方法

private static int hugeCapacity(int minCapacity) {
        //如果size+1之后比0小,也就是整形溢出的时候,直接抛异常
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        //否则如果比整形最大值-8大 就取整形最大值Integer.MAX_VALUE,否则取整形最大值-8
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

至此整个扩容操作完成。

2、指定位置添加元素add方法

public void add(int index, E element)

    public void add(int index, E element) {
        //这个函数很简单主要是检查index下标是否越界,否则抛异常
        rangeCheckForAdd(index);
        //这个函数上面分析过了,不再解释
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //这个函数可能大家没见过,他的作用是将elementData数组的index位置开始size -index长度的        
       //元素全部拷贝到elementData 数组index+1的位置,也就是将index位置开始的数组全部后移一      
       //位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        //再将插入的元素赋值给index位置
        elementData[index] = element;
        //数组元素加1
        size++;
    }

三、remove方法

1.根据下标删除的remove方法

public E remove(int index) {
        //检查下标是否越界
        rangeCheck(index);
        //修改次数加1
        modCount++;
        E oldValue = elementData(index);
        //需要移动多少个元素 如数组有5个元素 0,1,2,3,4,index等于2就是5-2-1=2,需要移动两        
          //个元素
        int numMoved = size - index - 1;
        //如果移动的元素个数大于0
        if (numMoved > 0)
            //这个函数之前介绍过,之前是移动index之后的元素到index+1,现在恰好相反,把index+1    
             //后面的元素移动到index
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //把最后一个元素置为null,方便jvm回收,也是上面如果移动的元素个数等于0 ,也就是要删除的 
        //是最后一个元素时,直接置为空
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

2.根据元素删除的remove方法

public boolean remove(Object o) {
        //如果元素等于null,则进入此逻辑
        if (o == null) {
            //遍历数组
            for (int index = 0; index < size; index++)
                //如果遇到元素为null的
                if (elementData[index] == null) {
                    //快速删除,这个函数下面会讲
                    fastRemove(index);
                    return true;
                }
        } else {
         //如果要删除的元素不等于null
            for (int index = 0; index < size; index++)
                //遍历过程遇到相等的,则删除
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

来看一下fastRemove方法:

    /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    //你发现了什么?哎好像就是走之前根据索引删除的逻辑 一摸一样啊
    //但是仔细看 你会发现少了一个下标越界的检查操作,看头顶这行注释,为什么?该方法是私有方法
     //调用的地方已经保证index在0到size-1之间,所以这个方法名fastRemove挺有意思
    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
    }

三、迭代操作

我不知道你面试的时候,有没有被问到过,ArrayList在循环的时候,能删除元素吗?如果你看过面试题,肯定知道答案是不行,那我们来看看为啥不行。这就和上面频繁出现的modCount有关了。

首先我们要知道ArrayList有iterator()方法,这个方法的来源是Collection接口继承了Iterable接口,实现这个接口说明该类是可迭代的,而iterator方法的返回值 类型Iterator成为迭代器,是需要自己实现的,这个类似于Comparable和Comparator的关系。

知道了这个,那我们对增强for循环一定不陌生,那凭什么ArrayList可以用增强 for循环,我们自己的类却不行?原因是增强for底层用的是迭代器操作,我们只要自己实现Iterable接口,也能使用增强for。

public interface Collection<E> extends Iterable<E>
 public Iterator<E> iterator() {
        return new Itr();
    }

接下来看一下问题关键:ArrayList的迭代器实现

 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;

        Itr() {}

        public boolean hasNext() {
            //游标是否已到最大值
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
              //这是关键所在,检查修改次数,不一样会抛异常,下面方法写了,而上面我们看到无论是添 
           //加元素还是删除元素modCount都会加1因此过不了这里的校验
            checkForComodification();

            int i = cursor;
            //如果游标大于数组长度,抛出异常
            if (i >= size)
                throw new NoSuchElementException();
            //这里可能大家会有疑问,上面不是已经判断游标是否越界了吗怎么还要判断
            //其实这边应该是为了防止多线程并发修改的情况。比如{1,2,3,4,5}有五个元素,这时候线程 
             //1在遍历,游标 刚好在索引4这个位置,线程2此时删除了一个元素,这个时候数组长度只有4 
            //了,此时显然取不到那个元素,因此此处直接抛出异常
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
             //游标+1
            cursor = i + 1;
            //获取对应位置的元素,并赋值给lastRet
            return (E) elementData[lastRet = i];
        }

        //这是迭代器自己的删除方法,在迭代中是可用的,我们来看下是为什么
        public void remove() {
            //    如果一次都没迭代过,则抛异常,只要调用了next,lastRet都会有值
            if (lastRet < 0)
                throw new IllegalStateException();
            //检查状态
            checkForComodification();

            try {
                //调用arraylist自己的remove方法,哎?那按理说modCount++会抛异常啊,为啥没有
                //来看下面代码
                ArrayList.this.remove(lastRet);
                //next之后游标会加1,但是我们删除了元素,所以把游标重置到当前位置
                cursor = lastRet;
                 //这里是重置lastRet为-1,删除之后必须迭代到下一个
                lastRet = -1;
                //这行代码你发现了什么?modCount又赋值给了expectedModCount,所以不会报错
                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源码的解析就先写到这里,原创不易,注释都是一行一行手敲的,转载请注明出处。欢迎大家进行评论和点赞,有什么问题也可以加我私人微信:zyj-jy66。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章