源码详解数据结构Linked List

x33g5p2x  于2022-03-17 转载在 其他  
字(7.7k)|赞(0)|评价(0)|浏览(429)

本文分享自华为云社区《LinkedList 源码分析》,作者: 陈皮的JavaLib。

LinkedList 简介

java.util.Linked List 是 Java 集合框架中的成员之一,底层是基于双向链表实现,集合容量可动态变化的。它继承自 Abstract Sequential List 抽象类,实现了 List 接口。同时还实现了 Cloneable 和 Serializable 三个标记接口,说明 Array List 是可克隆复制的,可序列化的。

Array List 数组列表底层是基于动态数组实现的,所以优点是能支持快速随机访问,但是增删操作可能会比较慢(因为可能需要进行数组扩容,数据拷贝)。而且数组需要先申请一定的内存空间,可能会造成浪费。而链表列表 LinkedList 的优点是增删操作速度比较快,而且列表存储多少元素就动态申请多少节点来存储,比较节省内存空间。

为何要使用双向链表呢,主要在于遍历效率比单向链表高。例如当我们需要查找指定下标的节点,在指定下标进行增删改操作时,先判断这个位置是靠近头部还是尾部,从而决定从头部还是从尾部开始查找,提高效率。

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

2 LinkedList 源码分析

2.1 内部变量

LinkedList 的元素是存储在节点对象中的,节点类是 LinkedList 类的一个内部私有静态类,源码如下所示:

  1. private static class Node<E> {
  2. E item;
  3. Node<E> next;
  4. Node<E> prev;
  5. Node(Node<E> prev, E element, Node<E> next) {
  6. this.item = element;
  7. this.next = next;
  8. this.prev = prev;
  9. }
  10. }

LinkedList 中定义了3个变量,一个代表当前列表的元素个数,另外两个变量指向链表的头部和尾部。以及它的父类 AbstractList 中的 modCount 变量,每次对链表的增删改操作都会使它加1。

  1. transient int size = 0;
  2. transient Node<E> first;
  3. transient Node<E> last;
  4. protected transient int modCount = 0;

2.2 构造函数

ArrayList 有2个构造函数,一个无参构造函数,另一个使用指定 Collection 集合来构造集合的构造函数。

无参构造函数,什么都没有操作。

  1. public LinkedList() {}

使用指定 Collection 集合来构造链表,如果 Collection 不能为 null ,否则会抛出 npe 。

  1. public LinkedList(Collection<? extends E> c) {
  2. this();
  3. addAll(c);
  4. }
  5. public boolean addAll(Collection<? extends E> c) {
  6. return addAll(size, c);
  7. }
  8. public boolean addAll(int index, Collection<? extends E> c) {
  9. checkPositionIndex(index);
  10. Object[] a = c.toArray();
  11. int numNew = a.length;
  12. if (numNew == 0)
  13. return false;
  14. Node<E> pred, succ;
  15. if (index == size) {
  16. succ = null;
  17. pred = last;
  18. } else {
  19. succ = node(index);
  20. pred = succ.prev;
  21. }
  22. for (Object o : a) {
  23. @SuppressWarnings("unchecked") E e = (E) o;
  24. Node<E> newNode = new Node<>(pred, e, null);
  25. if (pred == null)
  26. first = newNode;
  27. else
  28. pred.next = newNode;
  29. pred = newNode;
  30. }
  31. if (succ == null) {
  32. last = pred;
  33. } else {
  34. pred.next = succ;
  35. succ.prev = pred;
  36. }
  37. size += numNew;
  38. modCount++;
  39. return true;
  40. }

2.3 常用方法

  • public E getFirst()

获取链表的第一个元素,如果不存在第一个节点,抛出异常。

  1. public E getFirst() {
  2. final Node<E> f = first;
  3. if (f == null)
  4. throw new NoSuchElementException();
  5. return f.item;
  6. }
  • public E getLast()

获取链表的最后一个元素,如果链表为空,则抛出异常。

  1. public E getLast() {
  2. final Node<E> l = last;
  3. if (l == null)
  4. throw new NoSuchElementException();
  5. return l.item;
  6. }
  • public E removeFirst()

删除第一个元素,如果链表为空,则抛出异常。

  1. public E removeFirst() {
  2. final Node<E> f = first;
  3. if (f == null)
  4. throw new NoSuchElementException();
  5. return unlinkFirst(f);
  6. }
  • public E removeLast()

删除最后一个元素,如果链表为空,则抛出异常。

  1. public E removeLast() {
  2. final Node<E> l = last;
  3. if (l == null)
  4. throw new NoSuchElementException();
  5. return unlinkLast(l);
  6. }
  • public void clear()

情况链表,遍历每一个节点,将每一个节点的内部引用都置为 null ,便于进行垃圾回收。

  1. public void clear() {
  2. for (Node<E> x = first; x != null; ) {
  3. Node<E> next = x.next;
  4. x.item = null;
  5. x.next = null;
  6. x.prev = null;
  7. x = next;
  8. }
  9. first = last = null;
  10. size = 0;
  11. modCount++;
  12. }
  • public boolean add(E e)

在链表尾部添加一个元素。

  1. public boolean add(E e) {
  2. linkLast(e);
  3. return true;
  4. }
  • public Iterator iterator()

获取 list 的迭代器,用于遍历集合中的元素。

  1. public Iterator<E> iterator() {
  2. return new Itr();
  3. }
  • public int size():返回集合元素个数。
  • public boolean contains(Object o):是否包含某个元素。
  • public boolean remove(Object o):删除某个元素。
  • public E get(int index):获取指定下标的元素。
  • public E set(int index, E element):在指定下标修改元素值。
  • public void add(int index, E element):在指定下标添加元素。

3 常见面试题分析

3.1 LinkedList 是线程安全的吗?

我们通过分析源码可知,对它的任何操作都是没有加锁的,所以在多线程场景下,它是线程不安全的。它适合在非多线程使用场景下,并且增删操作比较多的情况。

  1. public static void main(String[] args) throws InterruptedException {
  2. LinkedList<String> list = new LinkedList<>();
  3. Thread thread1 = new Thread(() -> {
  4. for (int i = 0; i < 1000; i++) {
  5. list.add(Thread.currentThread().getName() + i);
  6. }
  7. }, "Thread01");
  8. thread1.start();
  9. Thread thread2 = new Thread(() -> {
  10. for (int i = 0; i < 1000; i++) {
  11. list.add(Thread.currentThread().getName() + i);
  12. }
  13. }, "Thread02");
  14. thread2.start();
  15. thread1.join();
  16. thread2.join();
  17. System.out.println(list.size()); // 输出不一定是2000,例如1850
  18. }

如果增删操作比较多的话,可以使用 LinkedList ,LinkedList 增删操作速度比较快。

如果需要线程安全的话,可以使用 JDK 集合中的工具类 Collections 提供一个方法 synchronizedList 可以将线程不安全的 List 集合变成线程安全的集合对象,如下所示。

  1. public static void main(String[] args) throws InterruptedException {
  2. LinkedList<String> list = new LinkedList<>();
  3. // 封装成线程安全的集合
  4. List<String> synchronizedList = Collections.synchronizedList(list);
  5. Thread thread1 = new Thread(() -> {
  6. for (int i = 0; i < 1000; i++) {
  7. synchronizedList.add(Thread.currentThread().getName() + i);
  8. }
  9. }, "Thread01");
  10. thread1.start();
  11. Thread thread2 = new Thread(() -> {
  12. for (int i = 0; i < 1000; i++) {
  13. synchronizedList.add(Thread.currentThread().getName() + i);
  14. }
  15. }, "Thread02");
  16. thread2.start();
  17. thread1.join();
  18. thread2.join();
  19. System.out.println(synchronizedList.size());
  20. }

3.2 LinkedList 优缺点

  • 优点:增删操作速度快,不仅有头部和尾部双指针,可以根据要操作的下标靠近哪边,从而决定从哪一边开始遍历找到指定的下标。找到位置后,删除和插入操作的时间复杂度为 O(1) 。
  • 缺点:不支持快速随机访问,相对 ArrayList 比较慢,但也不是决定的,取决于列表的长度,以及访问的下标位置。

3.3 使用迭代器 Iterator 过程中,可以增删元素吗?

通过源码分析,在获取集合的迭代器方法中,返回的是 AbstractList 抽象类中定义的 ListItr 迭代器对象,在他的父类 Itr 中持有变量 expectedModCount ,在初始化迭代器对象时这个变量的值被赋予此时链表中的操作次数 modCount 。在迭代获取元素时,会检查这两变量是否相等,不相等则抛出并发修改异常。所以不支持在使用迭代器的过程中,对原链表进行增删改操作。但是可以调用迭代器的增删操作。

  1. private class ListItr extends Itr implements ListIterator<E> {
  2. ListItr(int index) {
  3. cursor = index;
  4. }
  5. public boolean hasPrevious() {
  6. return cursor != 0;
  7. }
  8. public E previous() {
  9. checkForComodification();
  10. try {
  11. int i = cursor - 1;
  12. E previous = get(i);
  13. lastRet = cursor = i;
  14. return previous;
  15. } catch (IndexOutOfBoundsException e) {
  16. checkForComodification();
  17. throw new NoSuchElementException();
  18. }
  19. }
  20. public int nextIndex() {
  21. return cursor;
  22. }
  23. public int previousIndex() {
  24. return cursor-1;
  25. }
  26. public void set(E e) {
  27. if (lastRet < 0)
  28. throw new IllegalStateException();
  29. checkForComodification();
  30. try {
  31. AbstractList.this.set(lastRet, e);
  32. expectedModCount = modCount;
  33. } catch (IndexOutOfBoundsException ex) {
  34. throw new ConcurrentModificationException();
  35. }
  36. }
  37. public void add(E e) {
  38. checkForComodification();
  39. try {
  40. int i = cursor;
  41. AbstractList.this.add(i, e);
  42. lastRet = -1;
  43. cursor = i + 1;
  44. expectedModCount = modCount;
  45. } catch (IndexOutOfBoundsException ex) {
  46. throw new ConcurrentModificationException();
  47. }
  48. }
  49. }
  50. private class Itr implements Iterator<E> {
  51. /**
  52. * Index of element to be returned by subsequent call to next.
  53. */
  54. int cursor = 0;
  55. /**
  56. * Index of element returned by most recent call to next or
  57. * previous. Reset to -1 if this element is deleted by a call
  58. * to remove.
  59. */
  60. int lastRet = -1;
  61. /**
  62. * The modCount value that the iterator believes that the backing
  63. * List should have. If this expectation is violated, the iterator
  64. * has detected concurrent modification.
  65. */
  66. int expectedModCount = modCount;
  67. public boolean hasNext() {
  68. return cursor != size();
  69. }
  70. public E next() {
  71. checkForComodification();
  72. try {
  73. int i = cursor;
  74. E next = get(i);
  75. lastRet = i;
  76. cursor = i + 1;
  77. return next;
  78. } catch (IndexOutOfBoundsException e) {
  79. checkForComodification();
  80. throw new NoSuchElementException();
  81. }
  82. }
  83. public void remove() {
  84. if (lastRet < 0)
  85. throw new IllegalStateException();
  86. checkForComodification();
  87. try {
  88. AbstractList.this.remove(lastRet);
  89. if (lastRet < cursor)
  90. cursor--;
  91. lastRet = -1;
  92. expectedModCount = modCount;
  93. } catch (IndexOutOfBoundsException e) {
  94. throw new ConcurrentModificationException();
  95. }
  96. }
  97. final void checkForComodification() {
  98. if (modCount != expectedModCount)
  99. throw new ConcurrentModificationException();
  100. }
  101. }

3.4 LinkedList 可以存储 null 值吗?元素可以重复吗?

LinkedList 底层是由双向链表实现的,并且在添加元素的时候,没有对元素进行值校验,所以可以存储 null 值,并且存储的元素是可以重复的。

  1. public boolean add(E e) {
  2. linkLast(e);
  3. return true;
  4. }
  5. void linkLast(E e) {
  6. final Node<E> l = last;
  7. final Node<E> newNode = new Node<>(l, e, null);
  8. last = newNode;
  9. if (l == null)
  10. first = newNode;
  11. else
  12. l.next = newNode;
  13. size++;
  14. modCount++;
  15. }

3.5 如何边遍历 ArrayList 元素,边删除指定元素?

不支持在遍历的同时对原链表进行操作,会抛出 ConcurrentModificationException 并发修改异常,前面我们提到使用迭代器 Iterator 遍历集合时,不能对集合进行增删操作(会导致 modCount 值变化)。应该使用 Iterator 类的 remove 方法。

  1. package com.chenpi;
  2. import java.util.Iterator;
  3. import java.util.LinkedList;
  4. /**
  5. * @author 陈皮
  6. * @version 1.0
  7. * @description
  8. * @date 2022/3/1
  9. */
  10. public class ChenPi {
  11. public static void main(String[] args) {
  12. LinkedList<String> list = new LinkedList<>();
  13. list.add("Java");
  14. list.add("C++");
  15. list.add("Python");
  16. list.add("Lua");
  17. Iterator<String> iterator = list.iterator();
  18. while (iterator.hasNext()) {
  19. String next = iterator.next();
  20. if ("C++".equals(next)) {
  21. iterator.remove();
  22. continue;
  23. }
  24. System.out.println(next);
  25. }
  26. }
  27. }
  28. // 输出结果如下
  29. Java
  30. Python
  31. Lua

点击关注,第一时间了解华为云新鲜技术~

相关文章