小言_互联网的博客

Java集合源码浅析(一) : ArrayList

420人阅读  评论(0)

(尊重劳动成果,转载请注明出处:https://yangwenqiang.blog.csdn.net/article/details/105418475冷血之心的博客)

背景

一直都有这么一个打算,那就是将Java中常见集合的源码进行一个全面的梳理(尽管已经有很多人进行了梳理总结)。重复造轮子的意义就在于,笔者可以亲自去阅读与欣赏JDK集合源码,学习JDK设计者们那优雅的code风格。

那么,就让我们从常用的集合ArrayList来看起吧。(注. 本系列文章所使用的JDK版本为1.8)

正文

ArrayList介绍

相信各位小伙伴都相当熟悉ArrayList的使用,不管是平时的日常工作,或者是面试时候的手撕算法中,我们都在频繁的使用ArrayList(当然,还有LinkedList)。

当然,对于面试题目:“ArrayList和LinkedList有什么区别?” 我们也可以倒背如流,分析的头头是道。

ArrayList和LinkedList的区别总结如下:

  • ArrayList底层使用了动态数组实现,实质上是一个动态数组
  • LinkedList底层使用了双向链表实现,可当作堆栈、队列、双端队列使用
  • ArrayList在随机存取方面效率高于LinkedList
  • LinkedList在节点的增删方面效率高于ArrayList
  • ArrayList必须预留一定的空间,当空间不足的时候,会进行扩容操作
  • LinkedList的开销是必须存储节点的信息以及节点的指针信息

接下来,我们先来依次分析ArrayList中关键性的源码吧。

类关键信息解析

ArrayList的实现原理是动态数组,它是线程不安全的,允许其中元素为null。ArrayList实现了 List, RandomAccess, Cloneable, java.io.Serializable接口,如下所示:

该类的继承关系图如下所示:

这里,笔者突然觉得RandomAccess比较陌生,顺势点开去看了下这个接口是干嘛用的?

答:其中RandomAccess代表了其拥有随机快速访问的能力,ArrayList可以以O(1)的时间复杂度去根据下标访问元素。

JDK源码中给出了这样的例子:

举个例子就是说我们的ArrayList实现了RandomAccess接口,表示具有了随机访问的能力,所以在遍历的时候,使用for循环,随机获取遍历速度更快;

而LinkedList没有实现该接口,不具备随机访问能力,所以遍历LinkedList的时候,我们优先使用迭代器来进行遍历。

比如说这段代码:

//        List<Integer> list = new ArrayList<>();
        List<Integer> list = new LinkedList<>();
        list.add(1);
        list.add(3);
        list.add(2);
        list.add(4);
        
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }

LinkedList执行起来效率会很低,大家可以点开看看list.get(i)到底经历了什么鬼,后续章节我们也会解析。

关键属性

既然,ArrayList底层实现是一个动态数组,那么来看下都有哪些基本属性吧。

// 用于序列化的版本号,这里可以忽略
private static final long serialVersionUID = 8683452581122892189L;

    /集合的初始容量为10
    private static final int DEFAULT_CAPACITY = 10;
    //指定大小的构造函数里可能使用到的空数组
    private static final Object[] EMPTY_ELEMENTDATA = {};
    //默认构造函数里的空数组
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    //真正存放元素的数组
    transient Object[] elementData;
    // 数组中真正包含的元素个数
    private int size;
    // 最大数组容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

至于说JDK8中为什么会有两个默认空数组?大家可以参考下这篇文章的介绍:
https://blog.csdn.net/weixin_43390562/article/details/101236833
简单理解就是说为了避免创建太多的空数组,减少内存的消耗,所以整了一个全局的空数组出来。

构造方法

三种构造方法:

   // 默认无参构造方法,初始容量是10,在add元素的时候,才会真正去初始化容量为10
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    // 指定容量创建ArrayList,分为容量大于0,等于0以及小于0,三种情况
    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);
        }
    }
    //将别的集合直接赋值进行创建的构造方法
    public ArrayList(Collection<? extends E> c) {
        //直接用toArray()的方法获取集合的数组对象,并且直接赋值给elementData
        elementData = c.toArray();
        size = elementData.length;
        // 这里是当c.toArray出错,没有返回Object[]时,利用Arrays.copyOf 来复制集合c中的元素到elementData数组中
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    }


如何扩容?

既ArrayList底层是基于动态数组实现的,那必不可少的会有一个扩容的过程吧?讲道理,应该是我们add元素的时候,是可能触发扩容过程的,接下里我们先来看下扩容方法grow( )的实现吧。

    //扩容方法
    private void grow(int minCapacity) {
        // 记录扩容前数组的长度
        int oldCapacity = elementData.length;
        //将原数组的长度扩大0.5倍作为扩容后新数组的长度(如果扩容前数组长度为10,那么经过扩容后的数组长度应该为15)
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //如果扩容后的长度小于当前数据量,那么就将当前数据量的长度作为本次扩容的长度
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //判断新数组长度是否大于可分配数组的最大大小
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            //将扩容长度设置为最大可用长度
            newCapacity = hugeCapacity(minCapacity);
        // 拷贝,扩容,构建一个新的数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    //判断如果新数组长度超过当前数组定义的最大长度时,就将扩容长度设置为Interger.MAX_VALUE,也就是int的最大长度
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE : MAX_ARRAY_SIZE;
        }


常用API

这里列出ArrayList中常用的API,后边会对其源码进行一定的浅析。

boolean add(E e)
将指定的元素添加到此列表的尾部。

void add(int index, E element)
将指定的元素插入此列表中的指定位置。

boolean addAll(Collection c)
按照指定 collection 的迭代器所返回的元素顺序,将该 collection 中的所有元素添加到此列表的尾部。

boolean addAll(int index, Collection c)
从指定的位置开始,将指定 collection 中的所有元素插入到此列表中。

void clear()
移除此列表中的所有元素。

Object clone()
返回此 ArrayList 实例的浅表副本。

boolean contains(Object o)
如果此列表中包含指定的元素,则返回 truevoid ensureCapacity(int minCapacity)
如有必要,增加此 ArrayList 实例的容量,以确保它至少能够容纳最小容量参数所指定的元素数。

E get(int index)
返回此列表中指定位置上的元素。

int indexOf(Object o)
返回此列表中首次出现的指定元素的索引,或如果此列表不包含元素,则返回 -1boolean isEmpty()
如果此列表中没有元素,则返回 true

int lastIndexOf(Object o)
返回此列表中最后一次出现的指定元素的索引,或如果此列表不包含索引,则返回 -1。

E remove(int index)
移除此列表中指定位置上的元素。

boolean remove(Object o)
移除此列表中首次出现的指定元素(如果存在)。

protected void removeRange(int fromIndex, int toIndex)
移除列表中索引在 fromIndex(包括)和 toIndex(不包括)之间的所有元素。

E set(int index, E element)
用指定的元素替代此列表中指定位置上的元素。

int size()
返回此列表中的元素数。

Object[] toArray()
按适当顺序(从第一个到最后一个元素)返回包含此列表中所有元素的数组。

T[] toArray(T[] a)
按适当顺序(从第一个到最后一个元素)返回包含此列表中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。

void trimToSize()
将此 ArrayList 实例的容量调整为列表的当前大小。


add增加元素

从上边列出的常见API中,我们可以看出当前存在四种add方法,我们先来看第一种:

/**
     * 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); 
        //在数组末尾添加上当前元素,并且修改size大小
        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++,此参数是指当前列表结构被修改的次数
        modCount++;
        // 判断当前数据量是否大于数组的长度
        if (minCapacity - elementData.length > 0)
            // 如果大于则进行扩容操作
            grow(minCapacity);
    }

这块需要注意的是DEFAULT_CAPACITY是初始容量,前面说了等于10,扩容grow方法我们在上边也介绍过了。

接下来,是指定index的add方法,本质上没有区别,只不过多了一个元素copy的过程,以及index check的逻辑:

	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++;
    }
	/**
     * A version of rangeCheck used by add and addAll.
     */
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }

然后就是添加一个完整集合的add方法:

public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }

指定index的add一个集合的方法:

public boolean addAll(int index, Collection<? extends E> c) {
        rangeCheckForAdd(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount

        int numMoved = size - index;
        if (numMoved > 0)
            System.arraycopy(elementData, index, elementData, index + numNew,
                             numMoved);

        System.arraycopy(a, 0, elementData, index, numNew);
        size += numNew;
        return numNew != 0;
    }

这两个方法也是一样的,判断是否需要扩容,然后进行了一个数组copy的过程。

add方法总结:

先判断是否越界,是否需要扩容;如果扩容, 就复制数组;然后设置对应下标元素值。

扩容过程:

  • 如果需要扩容的话,默认扩容一半。
  • 如果扩容一半不够,就用目标的size作为扩容后的容量。
  • 在扩容成功后,会修改modCount

get方法获取元素

既然是一个数组,那没啥好说的,check一下index,然后直接获取即可。

	/**
     * Returns the element at the specified position in this list.
     *
     * @param  index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        rangeCheck(index);

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

set方法设置元素

先看源码:

/**
     * Replaces the element at the specified position in this list with
     * the specified element.
     *
     * @param index index of the element to replace
     * @param element element to be stored at the specified position
     * @return the element previously at the specified position
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;
        return oldValue;
    }

注意,就是替换了index位置的元素值,但是这里返回值是一个该位置上的oldValue


remove删除元素

public E remove(int index) {
    rangeCheck(index);//判断是否越界
    modCount++;//修改modeCount 因为结构改变了
    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  //置空原尾部数据 不再强引用, 可以GC掉
    return oldValue;
}

这个删除指定位置index上的元素还是很好理解的,还是涉及到了一个元素的copy。接下来,再看一个删除指定Object的方法:

//删除该元素在数组中第一次出现的位置上的数据。 如果有该元素返回true,如果false。
public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);//根据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++;//修改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  //置空 不再强引用
}

删除操作一定会修改modCount,且可能涉及到数组的复制,相对低效,并且在批量删除中,涉及高效的保存两个集合公有元素的算法

说了增(add)删(remove)改(set)查(get)之后,我们再看看下其余常见的API方法吧。

indexOf方法:

这个我们就不贴源码了,感兴趣的可以看看,很简单,就是简单遍历找到该元素,找不到返回-1。另外,lastIndexOf方法则是从后开始找该元素。

contains方法

借助于indexOf方法实现,源码如下:

public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

clear方法

/**
     * Removes all of the elements from this list.  The list will
     * be empty after this call returns.
     */
    public void clear() {
        modCount++;

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

        size = 0;
    }

clear方法就是将数组中的所有位置都置为null,并且会修改modCount。

size和isEmpty方法

/**
     * Returns the number of elements in this list.
     *
     * @return the number of elements in this list
     */
    public int size() {
        return size;
    }

    /**
     * Returns <tt>true</tt> if this list contains no elements.
     *
     * @return <tt>true</tt> if this list contains no elements
     */
    public boolean isEmpty() {
        return size == 0;
    }

我们在前面介绍ArrayList属性的时候,介绍到了size属性,在add以及remove方法中会进行size++和size–。size直接记录了当前实际存在于数组中的元素,所以可以直接返回。

toArray方法

/**
     * Returns an array containing all of the elements in this list
     * in proper sequence (from first to last element).
     *
     * <p>The returned array will be "safe" in that no references to it are
     * maintained by this list.  (In other words, this method must allocate
     * a new array).  The caller is thus free to modify the returned array.
     *
     * <p>This method acts as bridge between array-based and collection-based
     * APIs.
     *
     * @return an array containing all of the elements in this list in
     *         proper sequence
     */
    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }

    /**
     * Returns an array containing all of the elements in this list in proper
     * sequence (from first to last element); the runtime type of the returned
     * array is that of the specified array.  If the list fits in the
     * specified array, it is returned therein.  Otherwise, a new array is
     * allocated with the runtime type of the specified array and the size of
     * this list.
     *
     * <p>If the list fits in the specified array with room to spare
     * (i.e., the array has more elements than the list), the element in
     * the array immediately following the end of the collection is set to
     * <tt>null</tt>.  (This is useful in determining the length of the
     * list <i>only</i> if the caller knows that the list does not contain
     * any null elements.)
     *
     * @param a the array into which the elements of the list are to
     *          be stored, if it is big enough; otherwise, a new array of the
     *          same runtime type is allocated for this purpose.
     * @return an array containing the elements of the list
     * @throws ArrayStoreException if the runtime type of the specified array
     *         is not a supertype of the runtime type of every element in
     *         this list
     * @throws NullPointerException if the specified array is null
     */
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        if (a.length < size)
            // Make a new array of a's runtime type, but my contents:
            return (T[]) Arrays.copyOf(elementData, size, a.getClass());
        System.arraycopy(elementData, 0, a, 0, size);
        if (a.length > size)
            a[size] = null;
        return a;
    }

这个方法也是常用的,可以将当前的list方便的转换为一个数组,底层实现还是一个数组的copy

迭代器 Iterator

public Iterator<E> iterator() {
    return new Itr();
}
/**
 * An optimized version of AbstractList.Itr
 */
private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return //默认是0
    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)//再次判断是否越界,在 我们这里的操作时,有异步线程修改了List
            throw new ConcurrentModificationException();
        cursor = i + 1;//游标+1
        return (E) elementData[lastRet = i];//返回元素 ,并设置上一次返回的元素的下标
    }

    public void remove() {//remove 掉 上一次next的元素
        if (lastRet < 0)//先判断是否next过
            throw new IllegalStateException();
        checkForComodification();//判断是否修改过

        try {
            ArrayList.this.remove(lastRet);//删除元素 remove方法内会修改 modCount 所以后面要更新Iterator里的这个标志值
            cursor = lastRet; //要删除的游标
            lastRet = -1; //不能重复删除 所以修改删除的标志位
            expectedModCount = modCount;//更新 判断集合是否修改的标志,
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
//判断是否修改过了List的结构,如果有修改,抛出异常
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

迭代器其实是一个内部类,包括Itr和ListItr,如下所示:

那么,这两种迭代器有啥区别呢?我们一起来看下,Iterator和ListIterator的区别如下:

  • 前者可以遍历list和set集合;后者只能用来遍历list集合
  • 前者只能前向遍历集合;后者可以前向和后向遍历集合
  • 后者实现了前者,增加了一些新的功能。

感兴趣的同学可以看下源码,也在ArrayList中哦,是一个内部类

ensureCapacity

这是一个重要但是不常用的API,ensureCapacity可以保证将当前数组扩容到足够容纳指定的个数,减少程序中的自动扩容次数,接下来我们举个例子:

ArrayList<Integer> list = new ArrayList<>();
        long begin1 = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            list.add(i);
        }
        System.out.println("cost time2:"+(System.currentTimeMillis()-begin1));

        ArrayList<Integer> list2 = new ArrayList<>();
        list2.ensureCapacity(10000);
        long begin2 = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            list2.add(i);
        }
        System.out.println("cost time2:"+(System.currentTimeMillis()-begin2));

上边这段代码,我们对list2在使用前提前进行了扩容,执行效率显著提升。

cost time2:4
cost time2:1

内部实现如下,主要基于grow方法进行扩容实现的。

/**
     * Increases the capacity of this <tt>ArrayList</tt> instance, if
     * necessary, to ensure that it can hold at least the number of elements
     * specified by the minimum capacity argument.
     *
     * @param   minCapacity   the desired minimum capacity
     */
    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
            // any size if not default element table
            ? 0
            // larger than default for default empty table. It's already
            // supposed to be at default size.
            : DEFAULT_CAPACITY;

        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

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

ArrayList源码总结

源码实在是太多,我们通过对常用API的分析,大概可以得出ArrayList的底层实现原理。总结如下:

  • ArrayList内部基于动态数组实现,所以可以进行扩容操作,每一次扩容增量都是50%
  • 基于数组实现的,所以内部多个API都避免不了对数组的copy操作,比如set和remove操作,所以导致ArrayList插入和删除效率低下
  • 基于数组实现,并且实现了RandomAccess,所以可以随机访问,根据index来找到元素的值,所以ArrayList获取元素的效率很高
  • 提供了多个迭代器,都是基于内部类实现的
  • 底层源码中没有做同步处理,所以是线程不安全的,之前的版本Vector原理基本一直,但是Vector在方法的实现上都会加上synchronized关键字
  • modeCount会在适当的时候进行++操作,可以实现快速失败

后续我会继续更新集合源码系列文章,欢迎大家关注交流~

如果对你有帮助,记得点赞哈,欢迎大家关注我的博客,关注公众号(文强的技术小屋),学习更多技术知识,一起遨游知识海洋~


转载:https://blog.csdn.net/qq_25827845/article/details/105418475
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场