java集合的实现细节--ArrayList和LinkedList

Posted 寻的足迹

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java集合的实现细节--ArrayList和LinkedList相关的知识,希望对你有一定的参考价值。

  ArrayList和LinkedList的实现差异 

  List代表一种线性表的数据结构,ArrayList则是一种顺序存储的线性表,ArrayList底层采用动态数组的形式保存每一个集合元素,LinkedList则是一种链式存储的线性表,其本质上就是一个双向链表,它不仅实现了List接口,还实现了Deque接口,Deque代表了一种双端队列,既具有队列(FIFO)的特性,也具有栈(FILO)的特性,也就是说,LinkedList既可以当成双向链表使用,也可以当成队列使用,还可以当成栈来使用。

public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable

  ArrayList底层采用一个elementData数组来保存所有集合元素,因此ArrayList在插入元素时需要完成两件事情:

  ①、保存ArrayList底层封装的数组长度大于集合元素的个数

  ②、将插入位置之后的所有数组元素“整体搬家”,向后移动一“格”

  当删除ArrayList集合中指定位置的元素时,程序也要进行“整体搬家”,而且还需要将被删除索引处的数组元素赋值为null,下面是ArrayList集合的remove(int 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; // clear to let GC do its work

        return oldValue;
    }

  对于ArrayList集合而言,当程序向ArrayList中添加、删除集合元素时,ArrayList底层都是需要对数组进行“整体搬家”,因此性能很差。

  但如果程序调用get(int index)方法取出ArrayList集合中的元素时,性能和数组几乎相同,效率非常快。下面是ArrayList集合的get(int index)方法的源代码。

  E elementData(int index) {
       return (E) elementData[index];
  }
     
  public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }    

  LinkedList本质上是一个双向链表,因此它使用内部类来保存每一个集合元素。

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

  从上面程序中的粗体字代码可以看出,一个Node对象代表双向链表的一个节点,该对象中的next变量指向下一个节点,previous子指向上一个节点。

  由于LinkedList采用双向链表来保存集合元素,因此它在添加集合元素时,只要对链表进行插入即可。下面是LinkedList添加节点的源码。

     public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

  从上面代码看出,由于Linked本质上就是一个双向链表,因此它非常方便的在指定节点之前插入新节点。

  上面add(int index,E element)方法实现中用到了以下三个方法。

  ①、node(int index):搜索指定索引出的元素

  ②、linkLast(E e):将e新节点插入到最后

  ③、linkBefore(E e,Node<E> succ):在succ节点之前插入e新节点

  node(int index)实际上就是get(int index)方法的底层实现。Linked必须逐个元素的搜索,直到找到第index个元素为止。node(int index)采用二分法的方式进行查找,以下是该方法的源代码。

   Node<E> node(int index) {
        // assert isElementIndex(index);
     //如果index小于size/2
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

  上面的node(int index)方法就是逐个元素的找到index索引处的元素,只是由于LinkedList是一个双向链表,因此程序先根据index的值判断它离链表头近还是离链表尾近,如果离链表头近则从头端开始搜索,如果离链表尾近则从尾端开始搜素。

  Linked的get(int index)方法只是对上面node(int index)方法的简单包装。get(int index)方法的源代码如下。

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

  但单纯的插入操作就比较简单了,只要修改几个节点里面的previous、next引用的值即可。下面是linkedbefore(E e, Node<E> succ)方法的源代码。

   void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        final Node<E> newNode = new Node<>(pred, e, succ);
        succ.prev = newNode;
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        size++;
        modCount++;
    }

  如果只是单纯的添加某个节点,那么LinkedList的性能是非常好的,但如果需要向某个指定索引处添加节点,LinkedList必须先找到指定索引处的节点,这个搜索过程的系统开销并不小,因此LinkedList的add(int index,E e)方法的性能并不是特别好。如果只使用LinkedList的addFrist(E e)、addLast(E e)、offerFrist(E e)、offerLast(E e)、pollFrist(E e)、pollLast(E e)的话,性能是非常好的,因为可以避免搜索过程。

  类似的,LinkedList为了实现remove(int index)方法,也必须先通过node(int index)方法找到index索引处的节点,然后修改它前一个节点的next引用,以及后一个节点的previous引用,下面是该方法的源代码。

    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

    E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

   综上所述,ArrayList、LinkedList有各自适用的场景,大部分情况下,ArrayList的性能总是优于Linkedlist,因此绝大部分都应该考虑使用ArrayList集合。但如果程序荆楚需要添加、删除元素,尤其是经常需要add(E e)方法向集合中添加元素,则应该考虑使用LinkedList集合了。

 




以上是关于java集合的实现细节--ArrayList和LinkedList的主要内容,如果未能解决你的问题,请参考以下文章

2018年4月16日JAVA

浅析java集合

浅析java集合

接口List

Java集合类的那点通俗的认知

java 18-11 Collections用于ArrayList集合中