LinkedList有以下几个特性:

  • LinkedList实现了List接口的同时,还实现了Deque接口,所以LinkedList也可以当双端队列使用;
  • LinkedList底层实现是双向链表,优点是删除或添加元素的效率高(时间复杂度是O(1)),查询效率低(时间复杂度为O(n));
  • LinkedList虽然实现了随机访问,但是效率低效,不建议使用;
  • 由于LinkedList底层是链表,不存在容量不足的问题,因此不像ArrayList一样还需要动态扩容机制;
  • LinkedList允许元素为null,且允许重复元素;
  • LinkedList是线程不安全的。

日常开发中如果需要用到队列或者栈这些数据结构时,建议使用LinkedList作为实现类。

1、常用API

1.1 常用API概览

  1. ***************************** 插入元素 *****************************
  2. // 将指定元素添加到链表末尾
  3. public boolean add(E e);
  4. // 将指定元素插入到链表的指定位置
  5. public void add(int index, E element);
  6. // 将指定元素插入到链表头
  7. public void addFirst(E e);
  8. // 将指定元素添加到链表的末尾
  9. public void addLast(E e);
  10. // 将指定集合中的所有元素添加到链表末尾
  11. public boolean addAll(Collection<? extends E> c);
  12. // 将指定集合中的所有元素从指定位置开始插入链表
  13. public boolean addAll(int index, Collection<? extends E> c);
  14. // 将指定元素添加到链表末尾(对应Queue里的offer)
  15. public boolean offer(E e);
  16. // 将指定元素添加到链表头
  17. public boolean offerFirst(E e);
  18. // 将指定元素添加到链表末尾
  19. public boolean offerLast(E e);
  20. // 将指定元素添加到链表头(对应Stack里的push)
  21. public void push(E e);
  22. ***************************** 删除元素 *****************************
  23. // 移除链表中的所有元素
  24. public void clear();
  25. // 移除链表的第一个元素,并返回该元素
  26. public E remove();
  27. // 移除链表中指定位置处的元素,并返回该元素
  28. public E remove(int index);
  29. // 若元素存在则从链表中该元素第一次出现的位置移除该元素
  30. public boolean remove(Object o);
  31. // 移除链表的第一个元素,并返回该元素
  32. public E removeFirst();
  33. // 移除链表最后一个元素,并返回该元素
  34. public E removeLast();
  35. // 从链表中移除指定元素第一次出现位置处的元素(以正序遍历)
  36. public boolean removeFirstOccurrence(Object o);
  37. // 链表中移除指定元素最后一次出现位置处的元素(以正序遍历)
  38. public boolean removeLastOccurrence(Object o);
  39. // 移除链表的第一个元素并返回(对应Queue里的poll)
  40. public E poll();
  41. // 移除链表的第一个元素并返回
  42. public E pollFirst();
  43. // 移除链表的最后一个元素并返回
  44. public E pollLast();
  45. // 移除链表的第一个元素并返回(对应Stack里的pop)
  46. public E pop();
  47. ***************************** 查找元素 *****************************
  48. // 判断链表中是否存在指定元素
  49. public boolean contains(Object o);
  50. // 返回链表中指定位置的元素
  51. public E get(int index);
  52. // 返回链表的第一个元素
  53. public E getFirst();
  54. // 返回链表的最后一个元素
  55. public E getLast();
  56. // 返回指定元素在链表中第一次出现的位置,若未找到则返回-1
  57. public int indexOf(Object o);
  58. // 返回指定元素在链表中最后一次出现的位置,若未找到则返回-1
  59. public int lastIndexOf(Object o);
  60. // 返回链表的第一个元素,不做删除(对应Stack里的peek)
  61. public E peek();
  62. // 返回链表的第一个元素,不做删除
  63. public E peekFirst();
  64. // 返回链表的最后一个元素,不做删除
  65. public E peekLast();
  66. // 返回链表的第一个元素(对应Queue里的element)
  67. public E element();
  68. ***************************** 修改元素 *****************************
  69. // 链表中指定位置处的元素修改为指定的元素,返回被替换的元素
  70. public E set(int index, E element);
  71. ***************************** 其他方法 *****************************
  72. // 返回链表中包含的元素的数量
  73. public int size();
  74. // 以适当的顺序(从第一个元素到最后一个元素)返回包含此链表中所有元素的数组
  75. public Object[] toArray();
  76. // 返回该链表的浅拷贝
  77. public Object clone();

LinkedList即可以当栈的实现类,又可以当队列的实现类,因此可以看到有些方法名就与Stack或者Queue里的方法名保持一致,方便习惯了api的人无缝切换到LinkedList的使用上来。

1.2 栈相关API

以下api与Stack类中的方法名保持一致,也推荐使用LinkedList定义的api:

// 将指定元素添加到链表头(对应Stack里的push)
public void push(E e);

// 返回链表的第一个元素,不做删除(对应Stack里的peek)
public E peek();

// 移除链表的第一个元素并返回(对应Stack里的pop)
public E pop();

1.3 队列相关API

以下api与Queue接口中的方法名保持一致,也推荐使用LinkedList定义的api:

// 将指定元素添加到链表末尾(对应Queue里的offer)
public boolean offer(E e);

// 返回链表的第一个元素,不做删除(对应Queue里的peek)
public E peek();

// 返回链表的第一个元素(对应Queue里的element)
public E element();

// 移除链表的第一个元素并返回(对应Queue里的poll)
public E poll();

2、源码浅析

先看一下LinkedList继承了哪些类,实现了哪些接口:

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

2.1 内部属性

// LinkedList中元素的数量
transient int size = 0;

// 头节点
transient Node<E> first;

// 尾节点
transient Node<E> last;

LinkedList底层是双向链表,该链表的节点的数据结构是内部类Node,如下:

private static class Node<E> {
    // node存放的元素的值
    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双向链表.png

如果看链表相关操作的源码吃力的话,建议先学习一下单链表的数据结构。

2.2 构造函数

// 无参构造函数
LinkedList<String> linkedList = new LinkedList<>();

// 有参构造函数,入参是一个集合
public LinkedList(Collection<? extends E> c);

2.3 add(int index, E element)

public void add(int index, E element) {
    // 检查插入索引是否合法
    checkPositionIndex(index);

    if (index == size)
        // 如果是尾插,则走专门的尾插方法
        linkLast(element);
    else
        // 否则走一般的插入方法
        linkBefore(element, node(index));
}

checkPositionIndex:

private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        // 又抛出了熟悉的异常
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

isPositionIndex:

private boolean isPositionIndex(int index) {
    // 插入索引的位置在头节点和尾结点之间
    return index >= 0 && index <= size;
}

linkLast:

// 尾插 && 头插
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    // 更新last尾节点指向新建的node节点
    last = newNode;
    // 如果当前链表一个元素都没有,则newNode就是头节点(头插)
    if (l == null)
        first = newNode;
    else
        // 让上一个尾节点的next指针指向newNode
        l.next = newNode;

    // LinkedList总元素个数+1
    size++;
    // LinkedList被修改次数+1,这一点跟ArrayList一样
    modCount++;
}

node:

// LinkedList源码中非常重要的一个方法,很多增删改查方法都会调用该方法
// 该方法输入一个index,返回该index对应的链表的Node节点
Node<E> node(int index) {
    // 也要先遍历一下链表才能获取指定位置的Node节点,出于提升遍历效率的考虑,这里有一定优化:
    // 遍历链表前会判断一下index在链表中的位置,如果在链表的前半部分,则从头节点开始向后遍历;
    // 如果在链表的后半部分,则从尾节点开始向前遍历,有点二分查找的意思
    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;
    }
}

linkBefore:

// 插入到链表中间部分
void linkBefore(E e, Node<E> succ) {
    // succ是调用node方法根据指定index获取到链表中的Node节点
    final Node<E> pred = succ.prev;
    // newNode的prev指针和next指针已经安排好了
    final Node<E> newNode = new Node<>(pred, e, succ);
    // succ节点只需更新prev指针,注意是newNode插入到当前节点succ的前面一个位置
    succ.prev = newNode;
    // 如果当前链表中只有一个节点,即succ是头结点,需单独处理,直接将头节点指向newNode
    if (pred == null)
        first = newNode;
    else
        // pred节点也只需更新prev指针
        pred.next = newNode;
    size++;
    modCount++;
}

如果这块看的迷糊的同学,建议刷一下leetCode里单链表插入节点的题。

2.4 remove(int index)

public E remove(int index) {
    // 跟add一样,做操作前先检查一下index值,严谨
    checkElementIndex(index);
    // 先调用node方法获取指定index的Node节点,再做删除操作
    return unlink(node(index));
}

checkElementIndex:

private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        // index非法都会抛出相同的异常
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

isElementIndex:

private boolean isElementIndex(int index) {
    // 判断index是否合法的规则跟isPositionIndex非法一样,必须在头节点和尾节点之间
    return index >= 0 && index < size;
}

unlink:

E unlink(Node<E> x) {
    // 先把被删除节点的三个成员都获取一遍
    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的三个成员引用在使用完毕后都指向null,便于垃圾回收
        x.prev = null;
    }

    if (next == null) {
        // 如果删除的节点是尾节点,直接将尾节点指向当前节点的前一个节点
        last = prev;
    } else {
        next.prev = prev;
        // 当x的三个成员引用在使用完毕后都指向null,便于垃圾回收
        x.next = null;
    }

    // 当x的三个成员引用在使用完毕后都指向null,便于垃圾回收,此时x节点在下一次垃圾回收时可以被gc了
    x.item = null;

    // 更新一下这两个值
    size--;
    modCount++;
    return element;
}

2.5 get(int index)

public E get(int index) {
    // 上面已分析过该方法
    checkElementIndex(index);
    // 先调用node方法获取指定index对应的Node节点,再返回该Node节点的val成员
    return node(index).item;
}

2.5 set(int index, E element)

public E set(int index, E element) {
    // 上面已分析过该方法
    checkElementIndex(index);

    // 调用node方法获取指定index对应的Node节点
    Node<E> x = node(index);
    // 获取当前index下之前的值,因为该方法的返回值是这个
    E oldVal = x.item;
    // 一个赋值操作,将x.item引用重新指向被set的新值
    x.item = element;
    return oldVal;
}

3、其他

3.1 ArrayList和LinkedList哪个更占空间?

先上结论:LinkedList更占空间。但也要分析一下ArryList和LinkedList中占内存空间的点是哪些:

  • ArryList:由于存在动态扩容机制,且是1.5倍,也就是说当刚触发一次扩容操作时,底层数组中只有将近2/3的空间被利用,还有1/3的空间都是空;
  • LinkedList:由于底层是双向链表,需要维护头指针和尾指针,虽然没有动态扩容机制,但内存空间占用更大。

    3.2 LinkedList真的插入比ArrayList快么?

    不一定,要取决插入的具体位置,但整体情况确实是LinkedList的插入比ArrayList快。

  • 如果ArrayList是尾插且不存在扩容的情况,ArrayList的插入操作不会比LinkedList慢,因为仅是一个底层数组的赋值操作;

  • 如果是插入在list的中间位置,LinkedList也要先遍历最多一半长度的list找到那个index,然后再做链表插入操作;而ArrayList有可能触发扩容操作,且一定会存在数组的copy动作,如果插入的位置靠近尾端,需要移动元素的个数会相对较少些,移动的元素个数是插入位置之后的元素个数。

    3.3 为什么使用LinkedList代替原有的Stack类?

    因为Stack的性能相比LinkedList低。因为 Stack 继承自 Vector, 而 Vector 在每个方法中都加了synchronized 锁;但是如果考虑线程安全的情况,不能使用LinkedList,Stack虽然线程安全,但也有比Stack更高效的方案,比如线程安全的集合类,或者在外部加锁。

    3.4 为什么遍历操作ArrayList比LinkedList效率高?

    因为ArryList底层是数组,会开辟一块连续的内存空间,LinkedList相邻节点在内存空间上基本不会连续,显然遍历操作在连续内存空间中更快。

    参考:

    https://www.jianshu.com/p/4a1347d92cef
    当面试官问我ArrayList和LinkedList哪个更占空间时,我这么答让他眼前一亮
    为什么不推荐 ArrayDeque 代替 Stack
    LinkedList详解