ArrayList底层源码剖析

一、概述

工作中我们会经常使用到集合,像ArrayList、LinkedList,也知道ArrayList 相比于LinkedList 它的查找、修改效率更高,但是新增、删除的效率不如LinkedList,究竟为什么会有这样的差别?希望通过这篇文章让你对这两种数据结构有更深刻的认识。


二、结构差异

首先从源码的角度来看(没有注明版本的情况下,默认为jdk1.8)

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;
    private static final int DEFAULT_CAPACITY = 10;
    private static final Object[] EMPTY_ELEMENTDATA = {};
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    transient Object[] elementData; // non-private to simplify nested class access
    /**
     * The size of the ArrayList (the number of elements it contains).
     *
     * @serial
     */
    private int size;
    ...
 }

以上为ArratList 部分源码(删除了部分注释),从中我们可以看出ArrayList 的数据是以Object 数组形式存储在内存里面的。数组我们都知道它有一个索引(index),我们可以通过索引来访问对应的项,于是可以推测出,查找ArrayList 里面的数据耗费的时间应该是O(1),下面贴上ArrayList get set 方法的源码

    /**
     * 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) {
        // 检测传入的index是否超出数组的大小,超出则报异常
        rangeCheck(index);
​
        return elementData(index);
    }
    /**
     * 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) {
        // 检测传入的index是否超出数组的大小,超出则报异常
        rangeCheck(index);
        // 将原索引存储的值取出来
        E oldValue = elementData(index);
        // 将原索引对应的值改为新值
        elementData[index] = element;
        // 返回原值
        return oldValue;
    }

从以上源码可以看出get set 方法确实和我们预期的一样,操作花费的是常数时间,下面我们再看看新增和删除(以下源码笔者删除了原英文注释添加了笔者自己的注释)

    /**
     * 将指定的元素追加到此列表的末尾。
     *
     * @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;
    }
    /**
     * 将指定的元素插入到列表中的指定位置。将当前位于该位置的元素(如果有的话)
     * 和后续元素向右移动(给它们的索引添加1)
     *
     * @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);
        // 确保数组的大小能够插入成功(容量不够会扩容)
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 将index之后的元素全部往右移一位(index+1)
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 在index位置插入该数据
        elementData[index] = element;
        size++;
    }
   /**
     * 移除列表中指定位置的元素。将后续元素左移(从它们的下标减去1)。.
     *
     * @param index the index of the element to be removed
     * @return the element that was removed from the list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E remove(int index) {
        // 判断给元素插入的索引是否合法
        rangeCheck(index);
​
        modCount++;
        // 取出旧值
        E oldValue = elementData(index);
​
        int numMoved = size - index - 1;
        // 删除最后一个数据时numMoved=0,此时只需要将数组大小减容一个单位即可
        if (numMoved > 0)
            // 将index后面的数据全部向左移以为(index+1)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
​
        return oldValue;
    }
​
    /**
     * 如果指定元素存在,则从列表中删除该元素的第一个匹配项。
     * 如果列表中不包含元素,则保持不变。
     * <tt>i</tt> such that
     * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>
     * (if such an element exists).  Returns <tt>true</tt> if this list
     * contained the specified element (or equivalently, if this list
     * changed as a result of the call).
     *
     * @param o element to be removed from this list, if present
     * @return <tt>true</tt> if this list contained the specified element
     */
    public boolean remove(Object o) {
        // 要删除的元素为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++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

通过上面的源码我们可以看出新增和删除会导致数据的index 值发生变化。最好的情况下,只需要在末尾添加值不需要改变其他数据的index ,最坏的情况下;在第一位插入数据,整个数组里面所有值都会变动。

分析完ArrayList,接下来再看看LinkedList

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    transient int size = 0;
    // 链表第一个节点
    transient Node<E> first;
    // 链表最后一个节点
    transient Node<E> last;
    
    ...
    // 链表节点
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;
​
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
    
    ...
    
}

通过LinkedList 的源码我们可以看出,LinkedList 是一个双向链表,我们之前介绍过 单链表,单链表和双链表的差异就是:单链表只能从前一个节点指向后一个节点,无法从后一个节点指向前一个节点,而双连表既能从前一个节点指向后一个节点也能从后一个节点指向前一个节点。之前在单链表这篇文章中指出:链表这种结构在内存中是无序的,只存在节点和节点之间“链”的关系,所以对链表的插入操作是没有过多的约束,只需要定义好“链”即可,下面看下源码

    /**
     * 将指定的元素追加到此列表的末尾。
     *
     * <p>This method is equivalent to {@link #addLast}.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    
     /**
     * Links e as last element.
     */
    void linkLast(E e) {
        // 取出当前链表最后一个节点
        final Node<E> l = last;
        // 新建一个节点(该节点的上一个节点定义为当前链表的最后一个节点)
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        // 如果最后一个节点为空,说明当前链表为空,即新插入的节点就为第一个节点
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

通过以上源码分析可以看出链表的插入无需其他条件,只需要维护好“链”的关系即可,删除只需要找到要被删除的节点,去除该节点和其他节点的“链”,详细的逻辑可以去看java基础数据结构之链表这篇文章,这里就不再赘述,链表的查找是需要耗费O(N) 时间,因为它只能从第一个节点或者最后一个节点通过“链”的关系一个一个去找。

分析到这里相信各位已经知道ArrayList查找和修改操作比LinkedList 效率高,但是插入和删除却不如LinkedList 的原因,是因为二者数据结构的不同导致的结果。


三、Iterator接口

Arraylist LinkedList 都实现了List 接口,而List 接口继承了Collection接口,Collection 接口又继承了Iterable 接口,也就是说ArrayList LinkedList 都间接实现了Iterable,实现了Iterable 接口的集合必须提供一个iterator 方法,该方法返回一个Iterator 类型的对象,下面为Iterator 的源码

public interface Iterator<E> {
    
    boolean hasNext();   
    
    E next();
    
    default void remove() {
        throw new UnsupportedOperationException("remove");
    }
    
    default void forEachRemaining(Consumer<? super E> action) {
        Objects.requireNonNull(action);
        while (hasNext())
            action.accept(next());
    }
}

Iterator 接口的思路是:通过iterator方法,每个集合均可创建并返回给用户一个实现Iterator 接口的对象,并将当前位置的概念在对象内部存储下来。

Iterator提供了四个方法next、hasNext、remove以及forEachRemaining

next方法的调用都给出集合的下一项,即第一次调用next 给出第1项,第二次调用给出第2项,依此类推。

hasNext方法用来告知是否存在下一项。

remove 可以删除由next 最新返回的值,它和Collection remove 方法相比的优点在于:后者的删除必须首先找到要被删除的项,才能进行删除操作,如果知道要被删除项的准确位置,那么前者便可减少很多开销了。

forEachRemaining方法是jdk8 新增加的方法,作用是处理集合中剩余的数据,使用方法如下

 List<Integer> integers = Arrays.asList(1, 3, 5, 7, 9);
 Iterator<Integer> iterator = integers.iterator();
 Integer next = iterator.next();
 // 将集合中剩余的数据打印出来(输出为 3,5,7,9 )
 iterator.forEachRemaining(integer -> System.out.println(integer));

四、小结

本章主要分析了ArrayList LinkedList 在数据结构上的差异,以及性能上的优缺点,介绍了Iterato接口(实现了Iterable 接口的集合能使用增强型for 循环)以及Iterator 接口里面的方法。

另外在使用无参构造器创建ArrayList 对象时,其是一个空的数组,当插入第一个值时,数组容量会变成10(默认容量大小)。如果我们知道要存放的数据量有多少就可以在new 对象的时候使用有参构造器给ArrayList 一个初始化容量,这样就可以避免频繁扩容导致的性能损耗。ArrayList 的扩容量为原有数组大小的一半,下面给出扩容源码

    /**
     * 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;
        // 扩容
        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);
    }

从源码中我们可以看出扩容量为 oldCapacity >> 1 (移位运算,向左移一位),该值其实等于 oldCapacity / 2。

需要jdk8源码的朋友可以关注本公众号发送:【源码】 即可获取java8的源码。


今日的分享就到这里,感兴趣的读者朋友可以关注本公众号获取更多内容



本人因所学有限,如有错误之处,望请各位指正!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值