ArrayList与LinkedList区别

ArrayList与LinkedList区别

基本结构

  • ArrayList

    transient Object[] elementData;
    
  • LinkedList

    class Node<E>
    {
        E item;//存储的元素
        LinkedList.Node<E> next;//指向下一个节点的指针
        LinkedList.Node<E> prev;//指向上一个节点的指针
    }
    

首先我们都知道,ArrayList是基于动态数组实现的数据结构

那么动态指的什么呢?

这里的动态指的是数组的大小是可以动态调整的,那么具体是怎么实现的呢?

下面,我们基于部分源码进行分析

自动增容

	//默认大小为10
    private static final int DEFAULT_CAPACITY = 10;
    //ArrayList扩容的最大容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * 动态扩容实现
     * @param minCapacity
     */
    private void grow(int minCapacity)
    {
        int oldCapacity = elementData.length;
        //新数组大小为原大小的1.5倍(源码中使用了移位运算法,右移一位相当于0.5倍,1+0.5就是原来的1.5倍)
        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);
    }

经过分析源码,我们可以发现,ArrayList默认数组大小为10,如果插入元素后容量超出数组当前的最大容量,就会触发自动扩容机制,自动扩容机制将创建一个新的数组(大小为原数组的1.5倍),然后将原数组的内容通过Arrays.copyOf复制到扩容后的新数组**(这是添加方法费时的关键)**。

但是对于LinkedList来说,由于其基于双向链表实现,不存在增容问题,在插入元素时只需要创建新的节点对象,拼接到尾结点即可。

给末尾添加元素

  • ArrayList

     /**
         * 给数组尾添加元素
         * @param e
         * @return
         */
        public boolean add(E e)
        {
            ensureCapacityInternal(size + 1);  //这里进行增容检测,如果容量不足将会自动增容,耗时的关键
            elementData[size++] = e;//将元素添加到末尾
            return true;
        }
    

    这里我们就可以发现,当ArrayList容量足够大时,给数组尾部添加元素的操作效率将非常高。

  • LinkedList

        /**
         * 给尾部添加新元素
         * @param e
         */
        void linkLast(E e)
        {
            //尾结点位置
            final LinkedList.Node<E> l = last;
            //将插入的元素构建成新的节点
            final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
            //尾结点指向当插入的节点
            last = newNode;
            if (l == null)
            {
                first = newNode;
            }
            else
            {
                l.next = newNode;
            }
            size++;
            modCount++;
        }
    

    同样,尾插时效率也很高,但是由于ArrayList存在动态增容的可能,所以平均情况下,添加效率LinkedList较高

给指定位置添加元素

  • ArrayList

    /**
     * 给指定位置添加元素
     * @param index
     * @param element
     */
    public void add(int index, E element)
    {
        rangeCheckForAdd(index);//首先检查插入位置的下标是否合法(下标在0~size之间合法)
        ensureCapacityInternal(size + 1);  // 进行动态增容判断
        //将插入位置的元素及其之后元素整体后移(这里也需要耗时)
        System.arraycopy(elementData, index, elementData, index + 1, size - index);
        //插入新元素到指定位置
        elementData[index] = element;
        //大小+1
        size++;
    }
    
    /**
     * 检查插入位置的下标是否合法
     * @param index
     */
    private void rangeCheckForAdd(int index)
    {
        if (index > size || index < 0)
        {
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
        }
    }
    

    当向数组指定位置插入元素时,其需要向后移动大量数据位置(复制操作),所以相对尾插来说效率较低。

删除元素

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; //释放最后一个位置,方便GC处理
    return oldValue;
}

删除元素和给指定位置插入元素类似,需要向前移动大量元素,这里就不进行过多赘述。

相比之下,LinkedList只需改变指针指向无需移动其他元素位置,所以在插入和删除方面,LinkedList效率也往往高于ArrayList

查找元素

  • ArrayList

        public E get(int index)
        {
            rangeCheck(index);
            return elementData(index);
        }
    
        E elementData(int index)
        {
            return (E) elementData[index];//直接根据下标获取
        }
    
  • LinkedList

    public E get(int index)
    {
        checkElementIndex(index);
        return node(index).item;
    }
    //遍历链表查找
    LinkedList.Node<E> node(int index)
    {
        if (index < (size >> 1))//如果下标位置小于链表长度的一半,则从前向后查找
        {
            LinkedList.Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        }
        else//如果下标位置大于等于链表长度的一半,则从后向前查找
        {
            LinkedList.Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
    

经过源码分析,我们可以发现,ArrayList的访问基于数组下标,其速度将非常快,但是LinkedList最坏情况下需要遍历一半长度的链表才能找到,所以综上:ArrayList的访问效率要远远高于LinkedList

总结

插入方面:

  • 默认情况下,比如调用ArrayList和LinkedList的add(e)方法,都是插入在最后,如果这种操作比较多,那么就用LinkedList,因为不涉及到扩容。
  • 如果调用ArrayList和LinkedList的add(index, e)方法比较多,就要具体考虑了,因为ArrayList可能会扩容,LinkedList需要遍历链表,这两种到底哪种更快,是没有结论的,得具体情况具体分析。
  • 如果是插入场景比较少,但经常需要查询的话,查询分两种,第一种就是普通遍历,也就是经常需要对List中的元素进行遍历,那么这两种是区别不大的,遍历链表和遍历数组的区别,第二种就是经常需要按指定下标获取List中的元素,如果这种情况如果比较多,那么就用ArrayList。

查询方面

  • 对于ArrayList,它可以按照数组下标快速的定位要查询元素,相比之下,LinkedList需要对底层链表进行遍历,才能找到指定下标的元素。
  • 如果我们讨论的是获取第一个元素,或最后一个元素,ArrayList和LinkedList在性能上是没有区别的,因为LinkedList(双端队列)中有两个属性分别记录了当前链表中的头结点和尾结点,并不需要遍历链表。

删除方面及指定位置插入

  • 对于ArrayList来说,其需要移动大量元素,这将浪费太多时间(插入时还有可能出现自动扩容问题,不但会影响时间也可能浪费空间)
  • 对于LinkedList来说,只需要移动指针指向即可。

内存占用方面

  • 一般情况下,由于LinkedList使用链表结构存储,其每个节点都需要单独的内存保存当前节点的前驱结点及后继节点,所以其内存占用应该较高。

  • 但是这也不是绝对的,如果数据量刚好超过了ArrayList的容量,将会触发自动增容机制(比如存储11个数据,在第存储第11个时将触发自动增容机制,容量将变为15,将有4个被浪费),这将浪费将近一半的容量。

    所以,如果数据量较大时,并且需要添加元素的话,LinkedList占用的空间不一定比ArrayList占用的空间小~

  • 如果需要对集合进行序列化操作,由于 ArrayList使用**transient关键字修饰,其中没有被使用的空间将不会被序列化**,这种情况下,ArrayList的空间相比LinkedList可能会少一些。

  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值