今天来介绍下LinkedList,LinkedList与ArrayList一样实现List接口,只是ArrayList是List接口的大小可变数组的实现,LinkedList是List接口链表的实现。基于链表实现的方式使得LinkedList在插入和删除时更优于ArrayList,而随机访问则比ArrayList逊色些。
构造图如下:
蓝色线条:继承
绿色线条:接口实现

LinkedList是基于链表结构的一种List,在分析LinkedList源码前有必要对链表结构进行说明。

 链表是由一系列非连续的节点组成的存储结构,简单分下类的话,链表又分为单向链表和双向链表,而单向/双向链表又可以分为循环链表和非循环链表,下面简单就这四种链表进行图解说明。

1.1.单向链表

单向链表就是通过每个结点的指针指向下一个结点从而链接起来的结构,最后一个节点的next指向null。

1.2.单向循环链表

单向循环链表和单向列表的不同是,最后一个节点的next不是指向null,而是指向head节点,形成一个“环”。

1.3.双向链表

从名字就可以看出,双向链表是包含两个指针的,pre指向前一个节点,next指向后一个节点,但是第一个节点head的pre指向null,最后一个节点的tail指向null。

1.4.双向循环链表

双向循环链表和双向链表的不同在于,第一个节点的pre指向最后一个节点,最后一个节点的next指向第一个节点,也形成一个“环”。而LinkedList就是基于双向循环链表设计的。

LinkedList简介

LinkedList定义

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
  1. LinkedList 是一个继承于AbstractSequentialList的双向循环链表。它也可以被当作堆栈、队列或双端队列进行操作。
  2. LinkedList 实现 List 接口,能对它进行队列操作。
  3. LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
  4. LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
  5. LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。
  6. LinkedList 是异步的。

LinkedList属性

看看LinkedList的底层的属性

private transient Entry<E> header = new Entry<E>(null, null, null);
private transient int size = 0;

LinkedList中提供了上面两个属性,其中size和ArrayList中一样用来计数,表示list的元素数量,而header则是链表的头结点,Entry则是链表的节点对象。

private static class Entry<E> {
    E element;  // 当前存储元素
    Entry<E> next;  // 下一个元素节点
    Entry<E> previous;  // 上一个元素节点
    Entry(E element, Entry<E> next, Entry<E> previous) {
        this.element = element;
        this.next = next;
        this.previous = previous;
    }
}

Entry为LinkedList 的内部类,其中定义了当前存储的元素,以及该元素的上一个元素和下一个元素。结合上面双向链表的示意图很容易看懂。

LinkedList构造函数

/**
* 构造一个空的LinkedList .
*/
public LinkedList() {
    //将header节点的前一节点和后一节点都设置为自身
    header.next = header. previous = header ;
}

/**
* 构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列
*/
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}
需要注意的是空的LinkedList构造方法,它将header节点的前一节点和后一节点都设置为自身,这里便说明LinkedList 是一个双向循环链表,如果只是单存的双向链表而不是循环链表,他的实现应该是这样的:

public LinkedList() {
    header.next = null;
    header. previous = null;
}

非循环链表的情况应该是header节点的前一节点和后一节点均为null。

LinkedList源码解析(基于JDK1.6.0_45)

增加

/**
 * 将一个元素添加至list尾部
 */
public boolean add(E e) {
   // 在header前添加元素e,header前就是最后一个结点,就是在最后一个结点的后面添加元素e
   addBefore(e, header);
    return true;
}
/**
 * 在指定位置添加元素
 */
public void add(int index, E element) {
    // 如果index等于list元素个数,则在队尾添加元素(header之前),否则在index节点前添加元素
    addBefore(element, (index== size ? header : entry(index)));
}

private Entry<E> addBefore(E e, Entry<E> entry) {
    // 用entry创建一个要添加的新节点,next为entry,previous为entry.previous,意思就是新节点插入entry前面,确定自身的前后引用,
    Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
     // 下面修改newEntry的前后节点的引用,确保其链表的引用关系是正确的
    // 将上一个节点的next指向自己
    newEntry. previous.next = newEntry;
    // 将下一个节点的previous指向自己
    newEntry. next.previous = newEntry;
    // 计数+1
     size++;
     modCount++;
     return newEntry;
}

header作为双向循环链表的头结点是不保存数据的,也就是说hedaer中的element永远等于null。

/**

  • 添加一个集合元素到list中
    */
    public boolean addAll(Collection<? extends E> c) {

    // 将集合元素添加到list最后的尾部
    

    return addAll(size , c);
    }

    /**

    • 在指定位置添加一个集合元素到list中
      */
      public boolean addAll(int index, Collection<? extends E> c) {
      // 越界检查
      if (index < 0 || index > size)

      throw new IndexOutOfBoundsException( "Index: "+index+
                                          ", Size: "+size );
      

      Object[] a = c.toArray();
      // 要插入元素的个数
      int numNew = a.length ;
      if (numNew==0)

      return false;
      

      modCount++;

      // 找出要插入元素的前后节点
      // 获取要插入index位置的下一个节点,如果index正好是list尾部的位置那么下一个节点就是header,否则需要查找index位置的节点
      Entry

      // 构造一个节点,确认自身的前后引用
      Entry<E> e = new Entry<E>((E)a[i], successor, predecessor);
      // 将插入位置上一个节点的下一个元素引用指向当前元素(这里不修改下一个节点的上一个元素引用,是因为下一个节点随着循环一直在变)
      predecessor. next = e;
      // 最后修改插入位置的上一个节点为自身,这里主要是为了下次遍历后续元素插入在当前节点的后面,确保这些元素本身的顺序
      predecessor = e;
      

      }
      // 遍历完所有元素,最后修改下一个节点的上一个元素引用为遍历的最后一个元素
      successor. previous = predecessor;

      // 修改计数器
      size += numNew;
      return true;
      }


删除

/**
 * 删除第一个匹配的指定元素
 */
public boolean remove(Object o) {
     // 遍历链表找到要被删除的节点
    if (o==null) {
        for (Entry<E> e = header .next; e != header; e = e.next ) {
            if (e.element ==null) {
                remove(e);
                return true;
            }
        }
    } else {
        for (Entry<E> e = header .next; e != header; e = e.next ) {
            if (o.equals(e.element )) {
                remove(e);
                return true;
            }
        }
    }
    return false;
}

private E remove(Entry<E> e) {
    if (e == header )
       throw new NoSuchElementException();

   // 被删除的元素,供返回
    E result = e. element;
   // 下面修正前后对该节点 大专栏  LinkedList源码学习的引用
   // 将该节点的上一个节点的next指向该节点的下一个节点
   e. previous.next = e.next;
   // 将该节点的下一个节点的previous指向该节点的上一个节点
   e. next.previous = e.previous;
   // 修正该节点自身的前后引用
    e. next = e.previous = null;
   // 将自身置空,让gc可以尽快回收
    e. element = null;
   // 计数器减一
    size--;
    modCount++;
    return result;
}

修改

/**
 * 修改指定位置索引位置的元素
 */
public E set( int index, E element) {
    // 查找index位置的节点
    Entry<E> e = entry(index);
    // 取出该节点的元素,供返回使用
    E oldVal = e. element;
    // 用新元素替换旧元素
    e. element = element;
    // 返回旧元素
    return oldVal;
}

set方法看起来简单了很多,只要修改该节点上的元素就好了,但是不要忽略了这里的entry()方法,重点就是它。

查询

方法entry()根据index查询节点,我们知道数组是有下标的,通过下标操作天然的支持根据index查询元素,而链表中是没有index概念呢,那么怎么样才能通过index查询到对应的元素呢,下面就来看看LinkedList是怎么实现的。

/**
 * 查找指定索引位置的元素
 */
public E get( int index) {
    return entry(index).element ;
}

/**
 * 返回指定索引位置的节点
 */
private Entry<E> entry( int index) {
    // 越界检查
    if (index < 0 || index >= size)
        throw new IndexOutOfBoundsException( "Index: "+index+
                                            ", Size: "+size );
    // 取出头结点
    Entry<E> e = header;
    // size>>1右移一位代表除以2,这里使用简单的二分方法,判断index与list的中间位置的距离
    if (index < (size >> 1)) {
        // 如果index距离list中间位置较近,则从头部向后遍历(next)
        for (int i = 0; i <= index; i++)
            e = e. next;
    } else {
        // 如果index距离list中间位置较远,则从头部向前遍历(previous)
        for (int i = size; i > index; i--)
            e = e. previous;
    }
    return e;
}

 现在知道了,LinkedList是通过从header开始index计为0,然后一直往下遍历(next),直到到底index位置。为了优化查询效率,LinkedList采用了二分查找(这里说的二分只是简单的一次二分),判断index与size中间位置的距离,采取从header向后还是向前查找。

 基于双向循环链表实现的LinkedList,通过索引Index的操作时低效的,index所对应的元素越靠近中间所费时间越长。而向链表两端插入和删除元素则是非常高效的(如果不是两端的话,都需要对链表进行遍历查找)。

是否包含

// 判断LinkedList是否包含元素(o)
public boolean contains(Object o) {
    return indexOf(o) != -1;
}

// 从前向后查找,返回“值为对象(o)的节点对应的索引”
// 不存在就返回-1
public int indexOf(Object o) {
    int index = 0;
    if (o==null) {
        for (Entry e = header .next; e != header; e = e.next ) {
            if (e.element ==null)
                return index;
            index++;
        }
    } else {
        for (Entry e = header .next; e != header; e = e.next ) {
            if (o.equals(e.element ))
                return index;
            index++;
        }
    }
    return -1;
}

// 从后向前查找,返回“值为对象(o)的节点对应的索引”
// 不存在就返回-1
public int lastIndexOf(Object o) {
    int index = size ;
    if (o==null) {
        for (Entry e = header .previous; e != header; e = e.previous ) {
            index--;
            if (e.element ==null)
                return index;
        }
    } else {
        for (Entry e = header .previous; e != header; e = e.previous ) {
            index--;
            if (o.equals(e.element ))
                return index;
        }
    }
    return -1;
}

 和public boolean remove(Object o) 一样,indexOf查询元素位于容器的索引位置,都是需要对链表进行遍历操作,很低效。

判断容量

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

/**
 * {@inheritDoc}
 *
 * <p>This implementation returns <tt>size() == 0 </tt>.
 */
public boolean isEmpty() {
    return size() == 0;
}

 和ArrayList一样,基于计数器size操作,容量判断很方便。

LinkedList实现的Deque双端队列

/**
 * Adds the specified element as the tail (last element) of this list.
 *
 * @param e the element to add
 * @return <tt> true</tt> (as specified by {@link Queue#offer})
 * @since 1.5
 */
public boolean offer(E e) {
    return add(e);
}

/**
 * Retrieves and removes the head (first element) of this list
 * @return the head of this list, or <tt>null </tt> if this list is empty
 * @since 1.5
 */
public E poll() {
    if (size ==0)
        return null;
    return removeFirst();
}

/**
 * Removes and returns the first element from this list.
 *
 * @return the first element from this list
 * @throws NoSuchElementException if this list is empty
 */
public E removeFirst() {
    return remove(header .next);
}

/**
 * Retrieves, but does not remove, the head (first element) of this list.
 * @return the head of this list, or <tt>null </tt> if this list is empty
 * @since 1.5
 */
public E peek() {
    if (size ==0)
        return null;
    return getFirst();
}

/**
 * Returns the first element in this list.
 *
 * @return the first element in this list
 * @throws NoSuchElementException if this list is empty
 */
public E getFirst() {
    if (size ==0)
       throw new NoSuchElementException();

    return header .next. element;
}

/**
 * Pushes an element onto the stack represented by this list.  In other
 * words, inserts the element at the front of this list.
 *
 * <p>This method is equivalent to {@link #addFirst}.
 *
 * @param e the element to push
 * @since 1.6
 */
public void push(E e) {
    addFirst(e);
}

/**
 * Inserts the specified element at the beginning of this list.
 *
 * @param e the element to add
 */
public void addFirst(E e) {
   addBefore(e, header.next );
}

对LinkedList以及ArrayList的迭代效率比较

结论:ArrayList使用最普通的for循环遍历比较快,LinkedList使用foreach循环比较快。

看一下两个List的定义:

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

注意到ArrayList是实现了RandomAccess接口而LinkedList则没有实现这个接口。

做foreach循环的时候,编译器默认会使用这个集合的Iterator

总结:

LinkedList知识概括

  1. LinkedList 实际上是通过双向链表去实现的;它包含一个非常重要的内部类:Entry;Entry是双向链表节点所对应的数据结构,它包括的属性有:当前节点所包含的值,上一个节点,下一个节点。
  2. 从LinkedList的实现方式中可以发现,它不存在LinkedList容量不足的问题。
  3. LinkedList的克隆函数,即是将全部元素克隆到一个新的LinkedList对象中。
  4. LinkedList实现java.io.Serializable。当写入到输出流时,先写入“容量”,再依次写入“每一个节点保护的值”;当读出输入流时,先读取“容量”,再依次读取“每一个元素”。
  5. 由于LinkedList实现了Deque,而Deque接口定义了在双端队列两端访问元素的方法。提供插入、移除和检查元素的方法。每种方法都存在两种形式:一种形式在操作失败时抛出异常,另一种形式返回一个特殊值(null 或 false,具体取决于操作)。

ArrayList和LinkedList的比较

  1. 顺序插入速度ArrayList会比较快,因为ArrayList是基于数组实现的;LinkedList则不同,每次顺序插入的时候LinkedList将new一个对象出来,再加上一些引用赋值的操作,所以顺序插入LinkedList必然慢于ArrayList。
  2. LinkedList里面不仅维护了待插入的元素,还维护了Entry的前置Entry和后继Entry,如果一个LinkedList中的Entry非常多,那么LinkedList将比ArrayList更耗费一些内存。
  3. ArrayList使用最普通的for循环遍历比较快,LinkedList使用foreach循环比较快;ArrayList的遍历效率会比LinkedList的遍历效率高一些。
  4. 有些说法认为LinkedList做插入和删除更快,这种说法其实是不准确的:
    (1)LinkedList做插入、删除的时候,慢在寻址,快在只需要改变前后Entry的引用地址。
    (2)ArrayList做插入、删除的时候,慢在数组元素的批量copy,快在寻址。
12-14 03:28
查看更多