• LinkedList 源码解析(JDK1.8)


    目录

    一. 前言

    二. 常用方法

    三. 源码解析

    3.1. 属性和内部类

    3.2. 构造函数

    3.3. 添加元素

     3.4. 获取元素

    3.5. 删除元素

    3.6. 迭代器

    3.6.1. 头到尾方向的迭代

    3.6.2. 尾到头方向的迭代

    3.6.3. add() 插入元素

    3.6.4. remove() 移除元素


    一. 前言

        LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作是一个顺序容器,又可以看作是一个队列(Queue),同时又可以看作一个栈(Stack)。这样看来,LinkedList简直就是个全能冠军。当你需要使用栈或者队列时,可以考虑使用LinkedList,一方面是因为Java官方已经声明不建议使用Stack类,更遗憾的是,Java里根本没有一个叫做Queue的类(它是个接口名字)。关于栈或队列,现在的首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)有着更好的性能。

    特点:
    1. 底层是一个双向链表结构:增删快,查询慢。
    2. 包含大量操作首尾元素的方法。
    3. 线程不安全。如果需要多个线程并发访问,可以先采用Collections.synchronizedList()方法对其进行包装。

    LinkedList 继承了 AbstractSequentialList,而 AbstractSequentialList 又继承于 AbstractList 。详见 ArrayList 的源码,阅读后我们知道,ArrayList 同样继承了 AbstractList , 所以LinkedList会有大部分方法和 ArrayList 相似。

    简单介绍一下家族成员:
    List:表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
    Deque:继承自 Queue 接口,具有双端队列的特性,支持从两端插入和删除元素,方便实现栈和队列等数据结构。
    Cloneable:表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。
    Serializable:表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
    补充:
    序列化是将对象转换成二进制流,持久化是将对象转换成介质数据(先转成二进制再持久化)。
    序列化跟反序列化,发生在OSI的表示层。

    二. 常用方法

    方法描述
    public E getFirst()返回此列表中的第一个元素。
    public E getLast()返回此列表中的最后一个元素。
    public E removeFirst()移除并返回此列表中的第一个元素。
    public E removeLast()移除并返回此列表中的最后一个元素。
    public void addFirst(E e)在此列表的开始处插入指定的元素。
    public void addLast(E e)将指定的元素添加到列表的结束位置。
    public boolean contains(Object o)返回 true如果这个列表包含指定元素。
    public int size()返回此列表中元素的数目。
    public boolean add(E e)将指定的元素添加到列表的结束位置。
    public boolean remove(Object o)从该列表中移除指定元素的第一个。
    public void clear()从此列表中移除所有的元素。
    public E get(int index)返回此列表中指定位置的元素。
    public E set(int index, E element)用指定元素替换此列表中指定位置的元素。
    public void add(int index, E element)在列表中指定的位置上插入指定的元素。
    public E remove(int index)移除此列表中指定位置的元素。所有后续元素左移(下标减1)。返回从列表中删除的元素。
    public int indexOf(Object o)返回此列表中指定元素的第一个出现的索引,-1:如果此列表不包含元素。
    public int lastIndexOf(Object o)返回此列表中指定元素的最后一个发生的索引,-1:如果此列表不包含元素。
    public E peek()返回此列表的头部。
    public E poll()返回并删除此列表的第一个元素。
    public E remove()返回并删除此列表的第一个元素。
    public boolean offer(E e)将指定的元素添加到列表的尾部(最后一个元素)。
    public boolean offerFirst(E e)在列表的前面插入指定的元素。
    public boolean offerLast(E e)在列表的结尾插入指定的元素。
    public E peekFirst()返回列表的第一个元素。
    public E peekLast()返回列表的最后一个元素。
    public E pollFirst()删除并返回列表的第一个元素。
    public E pollLast()删除并返回列表的最后一个元素。
    public void push(E e)将一个元素推到由该列表所表示的堆栈上。换句话说,在这个列表的前面插入元素。相当于addFirst(E)。
    public E pop()从该列表所表示的堆栈中弹出一个元素。换言之,移除并返回此列表的第一个元素。

    三. 源码解析

    3.1. 属性和内部类

    1. /**
    2. * 集合大小
    3. * 当前有多少个节点
    4. */
    5. transient int size = 0;
    6. /**
    7. * 头部节点
    8. * Invariant: (first == null && last == null) ||
    9. * (first.prev == null && first.item != null)
    10. */
    11. transient Node first;
    12. /**
    13. * 尾部节点
    14. * Invariant: (first == null && last == null) ||
    15. * (last.next == null && last.item != null)
    16. */
    17. transient Node last;
    1. private static class Node {
    2. E item; // 节点值
    3. Node next; // 指向的下一个节点(后继节点)
    4. Node prev; // 指向的前一个节点(前驱结点)
    5. // 初始化参数顺序分别是:前驱结点、本身节点值、后继节点
    6. Node(Node prev, E element, Node next) {
    7. this.item = element;
    8. this.next = next;
    9. this.prev = prev;
    10. }
    11. }

    3.2. 构造函数

    LinkedList 中有一个无参构造函数和一个有参构造函数。

    1. // 创建一个空的链表对象
    2. public LinkedList() {
    3. }
    4. // 接收一个集合类型作为参数,会创建一个与传入集合相同元素的链表对象
    5. public LinkedList(Collection c) {
    6. this();
    7. addAll(c);
    8. }

    3.3. 添加元素

    LinkedList 除了实现了List接口相关方法,还实现了 Deque 接口的很多方法,所以我们有很多种方式插入元素。我们这里以List接口中相关的插入方法为例进行源码讲解,对应的是 add()、addAll() 方法。
    add(E e):用于在 LinkedList 的尾部插入元素,即将新元素作为链表的最后一个元素,时间复杂度为 O(1)。
    add(int index, E element):用于在指定位置插入元素。这种插入方式需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。
    addAll(int index, Collection c):从指定位置开始,将指定集合中的所有元素插入此列表。当不是构造方法调用时,它会在其最后节点追加新的元素。
    offer(E e):内部调用 add(E e) 方法。
    offerFirst(E e):内部调用 addFirst(E e) 方法。
    offerLast(E e):内部调用 addLast(E e) 方法。
    push(E e):内部调用addFirst方法实现

    1. // 在链表尾部插入元素
    2. public boolean add(E e) {
    3. linkLast(e);
    4. return true;
    5. }
    6. // 在链表指定位置插入元素
    7. public void add(int index, E element) {
    8. // 下标越界检查
    9. checkPositionIndex(index);
    10. // 判断 index 是不是链表尾部位置
    11. if (index == size)
    12. // 如果是就直接调用 linkLast 方法将元素节点插入链表尾部即可
    13. linkLast(element);
    14. else
    15. // 如果不是则调用 linkBefore 方法将其插入指定元素之前
    16. linkBefore(element, node(index));
    17. }
    18. // 将元素节点插入到链表尾部
    19. void linkLast(E e) {
    20. // 将最后一个元素赋值(引用传递)给节点 l
    21. final Node l = last;
    22. // 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空
    23. final Node newNode = new Node<>(l, e, null);
    24. // 将 last 引用指向新节点
    25. last = newNode;
    26. // 判断尾节点是否为空
    27. // 如果 l 是 null,意味着这是第一次添加元素
    28. if (l == null)
    29. // 如果是第一次添加,将first赋值为新节点,此时链表只有一个元素
    30. first = newNode;
    31. else
    32. // 如果不是第一次添加,将新节点赋值给l(添加前的最后一个元素)的next
    33. l.next = newNode;
    34. size++;
    35. modCount++;
    36. }
    37. // 在指定元素之前插入元素
    38. void linkBefore(E e, Node succ) {
    39. // assert succ != null;断言 succ不为 null
    40. // 定义一个节点元素保存 succ 的 prev 引用,也就是它的前一节点信息
    41. final Node pred = succ.prev;
    42. // 初始化节点,并指明前驱和后继节点
    43. final Node newNode = new Node<>(pred, e, succ);
    44. // 将 succ 节点前驱引用 prev 指向新节点
    45. succ.prev = newNode;
    46. // 判断尾节点是否为空,为空表示当前链表还没有节点
    47. if (pred == null)
    48. first = newNode;
    49. else
    50. // succ 节点前驱的后继引用指向新节点
    51. pred.next = newNode;
    52. size++;
    53. modCount++;
    54. }
    1. /**
    2. * 从指定位置开始,将指定集合中的所有元素插入此列表。
    3. * 将当前位于该位置的元素(如果有)和任何后续元素向右移动(增加其索引)。
    4. * 新元素将按照指定集合的迭代器返回的顺序出现在列表中。
    5. *
    6. * @param 插入指定集合中第一个元素的索引
    7. * @param c 包含要添加到此列表的元素的集合
    8. * @return {@code true} 如果此列表因调用而更改,则为true
    9. * @throws IndexOutOfBoundsException {@inheritDoc}
    10. * @throws NullPointerException if the specified collection is null
    11. */
    12. public boolean addAll(int index, Collection c) {
    13. // 检查index是否越界 判断是否是有效下标 此时index必须为0
    14. checkPositionIndex(index);
    15. Object[] a = c.toArray();
    16. // 数组length
    17. int numNew = a.length;
    18. if (numNew == 0)
    19. return false;
    20. // 创建两个双向链表节点 pred末游节点 succ首节点 初始值为null
    21. Node pred, succ;
    22. if (index == size) {
    23. // 初始首节点为null
    24. succ = null;
    25. // 存储第一个node
    26. pred = last;
    27. } else {
    28. succ = node(index);
    29. pred = succ.prev;
    30. }
    31. // 此时开始创建双向链表进行链接
    32. for (Object o : a) {
    33. // 获取值
    34. @SuppressWarnings("unchecked") E e = (E) o;
    35. // 创建一个新的节点(上一个节点,当前数值,下一个节点)
    36. Node newNode = new Node<>(pred, e, null);
    37. if (pred == null)// 第一个节点时 他的上一个节点时null
    38. first = newNode; // 此时最后一个节点就是它本身
    39. else
    40. // 此时将上一个节点的下游节点链接就是当前生成的
    41. pred.next = newNode;
    42. // 记录当前末游节点
    43. pred = newNode;
    44. }
    45. // 此时数据链接完
    46. if (succ == null) {
    47. // 将最后一个节点赋值给last
    48. last = pred;
    49. // 如果存在首届
    50. } else {
    51. // 将最后一个的下游节点链接为第一个节点
    52. pred.next = succ;
    53. // 将第一个的上游节点链接为末游节点
    54. succ.prev = pred;
    55. }
    56. //size=length+1
    57. size += numNew;
    58. modCount++;
    59. return true;
    60. }
    1. public boolean offer(E e) {
    2. return add(e);
    3. }
    4. public boolean offerFirst(E e) {
    5. addFirst(e);
    6. return true;
    7. }
    8. public boolean offerLast(E e) {
    9. addLast(e);
    10. return true;
    11. }
    12. public void push(E e) {
    13. addFirst(e);
    14. }

     3.4. 获取元素

    LinkedList获取元素相关的方法一共有 3 个:
    getFirst():获取链表的第一个元素。
    getLast():获取链表的最后一个元素。
    get(int index):获取链表指定位置的元素。

    1. // 获取链表的第一个元素
    2. public E getFirst() {
    3. final Node f = first;
    4. if (f == null)
    5. throw new NoSuchElementException();
    6. return f.item;
    7. }
    8. // 获取链表的最后一个元素
    9. public E getLast() {
    10. final Node l = last;
    11. if (l == null)
    12. throw new NoSuchElementException();
    13. return l.item;
    14. }
    15. // 获取链表指定位置的元素
    16. public E get(int index) {
    17. // 下标检查
    18. checkElementIndex(index);
    19. // 返回链表中对应下标的元素
    20. return node(index).item;
    21. }

    添加元素时,都返回了node(int index)方法,我们分析一下该方法:

    1. // 返回指定下标的非空节点
    2. Node node(int index) {
    3. // 断言下标未越界
    4. // assert isElementIndex(index);
    5. // 如果index小于size的二分之一 从前开始查找(向后查找),反之向前查找
    6. if (index < (size >> 1)) {
    7. Node x = first;
    8. // 遍历,循环向后查找,直至 i == index
    9. for (int i = 0; i < index; i++)
    10. x = x.next;
    11. return x;
    12. } else {
    13. Node x = last;
    14. //遍历,循环向前查找,直至 i == index
    15. for (int i = size - 1; i > index; i--)
    16. x = x.prev;
    17. return x;
    18. }
    19. }

    get(int index) 或 remove(int index) 等方法内部都调用了该方法来获取对应的节点。
    不难看出,该方法通过比较索引值与链表 size 的一半大小来确定从链表头还是链表尾开始遍历。
    1. 如果索引值小于 size 的一半,就从链表头开始遍历
    2. 如果索引值大于 size 的一半,就从链表尾开始遍历。
    这样可以在较短的时间内找到目标节点,充分利用了双向链表的特性来提高效率。

    3.5. 删除元素

    LinkedList 删除元素相关的方法一共有 5 个:
    removeFirst() :删除并返回链表的第一个元素。
    removeLast() :删除并返回链表的最后一个元素。
    remove(E e) :删除链表中首次出现的指定元素,如果不存在该元素则返回 false。
    remove(int index) :删除指定索引处的元素,并返回该元素的值。
    void clear() :移除此链表中的所有元素。
    E pop():调用removeFirst()。
    E poll():判断first元素是否为空,不为空则调用unlinkFirst(E e)删除。
    E pollFirst:判断first元素是否为空,不为空则调用unlinkFirst(E e)删除。
    E pollLast:判断last元素是否为空,不为空则调用unlinkLast(E e)删除。

    1. // 删除并返回链表的第一个元素
    2. public E removeFirst() {
    3. final Node f = first;
    4. if (f == null)
    5. throw new NoSuchElementException();
    6. return unlinkFirst(f);
    7. }
    8. // 删除并返回链表的最后一个元素
    9. public E removeLast() {
    10. final Node l = last;
    11. if (l == null)
    12. throw new NoSuchElementException();
    13. return unlinkLast(l);
    14. }
    15. // 删除链表中首次出现的指定元素,如果不存在该元素则返回 false
    16. public boolean remove(Object o) {
    17. // 如果指定元素为 null,遍历链表找到第一个为 null 的元素进行删除
    18. if (o == null) {
    19. for (Node x = first; x != null; x = x.next) {
    20. if (x.item == null) {
    21. unlink(x);
    22. return true;
    23. }
    24. }
    25. } else {
    26. // 如果不为 null ,遍历链表找到要删除的节点
    27. for (Node x = first; x != null; x = x.next) {
    28. if (o.equals(x.item)) {
    29. unlink(x);
    30. return true;
    31. }
    32. }
    33. }
    34. return false;
    35. }
    36. // 删除链表指定位置的元素
    37. public E remove(int index) {
    38. // 下标越界检查,如果越界就抛异常
    39. checkElementIndex(index);
    40. return unlink(node(index));
    41. }

    我们可以看到,均调用了 unlink(Node x),对该方法解析:

    1. E unlink(Node x) {
    2. // 断言 x 不为 null
    3. // assert x != null;
    4. // 获取当前节点(也就是待删除节点)的元素
    5. final E element = x.item;
    6. // 获取当前节点的下一个节点
    7. final Node next = x.next;
    8. // 获取当前节点的前一个节点
    9. final Node prev = x.prev;
    10. // 如果前一个节点为空,则说明当前节点是头节点
    11. if (prev == null) {
    12. // 直接让链表头指向当前节点的下一个节点
    13. first = next;
    14. } else { // 如果前一个节点不为空
    15. // 将前一个节点的 next 指针指向当前节点的下一个节点
    16. prev.next = next;
    17. // 将当前节点的 prev 指针置为 null,方便 GC 回收
    18. x.prev = null;
    19. }
    20. // 如果下一个节点为空,则说明当前节点是尾节点
    21. if (next == null) {
    22. // 直接让链表尾指向当前节点的前一个节点
    23. last = prev;
    24. } else { // 如果下一个节点不为空
    25. // 将下一个节点的 prev 指针指向当前节点的前一个节点
    26. next.prev = prev;
    27. // 将当前节点的 next 指针置为 null,方便 GC 回收
    28. x.next = null;
    29. }
    30. // 将当前节点元素置为 null,方便 GC 回收
    31. x.item = null;
    32. size--;
    33. modCount++;
    34. return element;
    35. }

    unlink() 方法的逻辑如下:
    1. 首先获取待删除节点 x 的前驱和后继节点;
    2. 判断待删除节点是否为头节点或尾节点:
        1>. 如果 x 是头节点,则将 first 指向 x 的后继节点 next。
        2>. 如果 x 是尾节点,则将 last 指向 x 的前驱节点 prev。
        3>. 如果 x 既不是头节点也不是尾节点,执行下一步操作。
    3. 将待删除节点 x 的前驱的后继指向待删除节点的后继 next,断开 x 和 x.prev 之间的链接;
    4. 将待删除节点 x 的后继的前驱指向待删除节点的前驱 prev,断开 x 和 x.next 之间的链接;
    5. 将待删除节点 x 的元素置空,修改链表长度。

    1. public E pop() {
    2. return removeFirst();
    3. }
    4. public E poll() {
    5. // 判断first元素是否为空,不为空则调用unlinkFirst(E e)删除
    6. final Node f = first;
    7. return (f == null) ? null : unlinkFirst(f);
    8. }
    9. public E pollFirst() {
    10. // 判断first元素是否为空,不为空则调用unlinkFirst(E e)删除
    11. final Node f = first;
    12. return (f == null) ? null : unlinkFirst(f);
    13. }
    14. public E pollLast() {
    15. // 判断last元素是否为空,不为空则调用unlinkLast(E e)删除
    16. final Node l = last;
    17. return (l == null) ? null : unlinkLast(l);
    18. }

    3.6. 迭代器

    推荐使用 for-each 循环来遍历 LinkedList 中的元素,for-each 循环最终会转换成迭代器形式。

    LinkedList 遍历的核心就是它的迭代器的实现。

    1. // 双向迭代器
    2. private class ListItr implements ListIterator {
    3. // 表示上一次调用 next() 或 previous() 方法时经过的节点;
    4. private Node lastReturned;
    5. // 表示下一个要遍历的节点;
    6. private Node next;
    7. // 表示下一个要遍历的节点的下标,也就是当前节点的后继节点的下标;
    8. private int nextIndex;
    9. // 表示当前遍历期望的修改计数值,用于和 LinkedList 的 modCount 比较,判断链表是否被其他线程修改过。
    10. private int expectedModCount = modCount;
    11. …………
    12. }

    下面我们对迭代器 ListItr 中的核心方法进行详细介绍。

    3.6.1. 头到尾方向的迭代

    1. // 判断还有没有下一个节点
    2. public boolean hasNext() {
    3. // 判断下一个节点的下标是否小于链表的大小,如果是则表示还有下一个元素可以遍历
    4. return nextIndex < size;
    5. }
    6. // 获取下一个节点
    7. public E next() {
    8. // 检查在迭代过程中链表是否被修改过
    9. checkForComodification();
    10. // 判断是否还有下一个节点可以遍历,如果没有则抛出 NoSuchElementException 异常
    11. if (!hasNext())
    12. throw new NoSuchElementException();
    13. // 将 lastReturned 指向当前节点
    14. lastReturned = next;
    15. // 将 next 指向下一个节点
    16. next = next.next;
    17. nextIndex++;
    18. return lastReturned.item;
    19. }

    3.6.2. 尾到头方向的迭代

    1. // 判断是否还有前一个节点
    2. public boolean hasPrevious() {
    3. return nextIndex > 0;
    4. }
    5. // 获取前一个节点
    6. public E previous() {
    7. // 检查是否在迭代过程中链表被修改
    8. checkForComodification();
    9. // 如果没有前一个节点,则抛出异常
    10. if (!hasPrevious())
    11. throw new NoSuchElementException();
    12. // 将 lastReturned 和 next 指针指向上一个节点
    13. lastReturned = next = (next == null) ? last : next.prev;
    14. nextIndex--;
    15. return lastReturned.item;
    16. }

    3.6.3. add() 插入元素

    1. LinkedList list = new LinkedList<>();
    2. list.add("apple");
    3. list.add(null);
    4. list.add("banana");
    5. // Collection 接口的 removeIf 方法底层依然是基于迭代器
    6. list.removeIf(Objects::isNull);
    7. for (String fruit : list) {
    8. System.out.println(fruit);
    9. }

    3.6.4. remove() 移除元素

    1. // 从列表中删除上次被返回的元素
    2. public void remove() {
    3. // 检查是否在迭代过程中链表被修改
    4. checkForComodification();
    5. // 如果上次返回的节点为空,则抛出异常
    6. if (lastReturned == null)
    7. throw new IllegalStateException();
    8. // 获取当前节点的下一个节点
    9. Node lastNext = lastReturned.next;
    10. // 从链表中删除上次返回的节点
    11. unlink(lastReturned);
    12. // 修改指针
    13. if (next == lastReturned)
    14. next = lastNext;
    15. else
    16. nextIndex--;
    17. // 将上次返回的节点引用置为 null,方便 GC 回收
    18. lastReturned = null;
    19. expectedModCount++;
    20. }

  • 相关阅读:
    JavaScript之观察者模式
    NDAttributeList源码解析及测试
    02 【axios fetch 跨域】
    第14章 JVM(二)
    JS选择排序
    [SpringBoot] 父子项目搭建,过滤多模块发布到私仓
    cookie时效无限延长方案
    Qt 自定义event
    XSS 和 CSRF
    以TrueType为例谈字形描述
  • 原文地址:https://blog.csdn.net/mrluo735/article/details/133941054