标签搜索

目 录CONTENT

文章目录

LinkedList(JDK1.8)源码解析

WP&CZH
2023-07-28 / 0 评论 / 1 点赞 / 422 阅读 / 3,209 字 / 正在检测是否收录...

概述

LinkedList 是 Java 集合框架中一个重要的实现,其底层采用的双向链表结构。和 ArrayList 一样,LinkedList 也支持空值和重复值。由于 LinkedList 基于链表实现,存储元素过程中,无需像 ArrayList 那样进行扩容。但有得必有失,LinkedList 存储元素的节点需要额外的空间存储前驱和后继的引用。另一方面,LinkedList 在链表头部和尾部插入效率比较高,但在指定位置进行插入时,效率一般。原因是,在指定位置插入需要定位到该位置处的节点,此操作的时间复杂度为O(N)。最后,LinkedList 是非线程安全的集合类,并发环境下,多个线程同时操作 LinkedList,会引发不可预知的错误。

以上是对 LinkedList 的简单介绍,接下来,我将会对 LinkedList 常用操作展开分析,继续往下看吧。

继承体系

LinkedList 的继承体系较为复杂,继承自 AbstractSequentialList,同时又实现了 List 和 Deque 接口。继承体系图如下(删除了部分实现的接口):

在这里插入图片描述

LinkedList 继承自 AbstractSequentialList,AbstractSequentialList 又是什么呢?从实现上,AbstractSequentialList 提供了一套基于顺序访问的接口。通过继承此类,子类仅需实现部分代码即可拥有完整的一套访问某种序列表(比如链表)的接口。深入源码,AbstractSequentialList 提供的方法基本上都是通过 ListIterator 实现的,比如:

public E get(int index) {
    try {
        return listIterator(index).next();
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

public void add(int index, E element) {
    try {
        listIterator(index).add(element);
    } catch (NoSuchElementException exc) {
        throw new IndexOutOfBoundsException("Index: "+index);
    }
}

// 留给子类实现
public abstract ListIterator<E> listIterator(int index);

所以只要继承类实现了 listIterator 方法,它不需要再额外实现什么即可使用。对于随机访问集合类一般建议继承 AbstractList 而不是 AbstractSequentialList。LinkedList 和其父类一样,也是基于顺序访问。所以 LinkedList 继承了 AbstractSequentialList,但 LinkedList 并没有直接使用父类的方法,而是重新实现了一套的方法。

另外,LinkedList 还实现了 Deque (double ended queue),Deque 又继承自 Queue 接口。这样 LinkedList 就具备了队列的功能。比如,我们可以这样使用:

Queue<T> queue = new LinkedList<>();

除此之外,我们基于 LinkedList 还可以实现一些其他的数据结构,比如栈,以此来替换 Java 集合框架中的 Stack 类(该类实现的不好,《Java 编程思想》一书的作者也对此类进行了吐槽)。

关于 LinkedList 继承体系先说到这,下面进入源码分析部分。

源码分析

LinkedList<String> dataList = new LinkedList<>(); // 创建 LinkedList
dataList.add("test"); // 添加数据
dataList.add(1, "test1"); // 指定位置,添加数据
dataList.addFirst("first"); // 添加数据到头部
dataList.addLast("last"); // 添加数据到尾部
dataList.get(0); // 获取指定位置数据
dataList.getFirst(); // 获取头部数据
dataList.getLast(); // 获取尾部数据
dataList.remove(1); // 移除指定位置的数据
dataList.removeFirst(); // 移除头部数据
dataList.removeLast(); // 移除尾部数据
dataList.clear(); // 清空数据

构造方法

transient int size = 0; // 当前列表的节点个数
transient Node<E> first; // 第一个节点
transient Node<E> last; // 最后一个节点

/* 构造方法一 */
public LinkedList() {
}
/* 构造方法二 */
public LinkedList(Collection<? extends E> c) {
  this();
  addAll(c);
}

可以看到 LinkedList 有 三个成员变量和两个构造方法,这里需要注意一下成员变量前面的 transient 关键字。

transient 关键字:当对象被序列化时(写入字节序列到目标文件)时,transient阻止实例中那些用此关键字声明的变量持久化;当对象被反序列化时(从源文件读取字节序列进行重构),这样的实例变量值不会被持久化和恢复。
这篇文章有讲:https://znunwm.top/archives/java-zhong-de-transient-guan-jian-zi-xiang-jie
为什么要有这个关键字呢?因为这里要告诉虚拟机,这三个成员变量不是 LinkedList 的永久性变量。

下面来分析一下构造方法二中的 addAll() 方法。

public boolean addAll(Collection<? extends E> c) {
  return addAll(size, c);
}

public boolean addAll(int index, Collection<? extends E> c) {
  // 检查 index 是否越界
  checkPositionIndex(index);

  Object[] a = c.toArray();
  int numNew = a.length;
  if (numNew == 0) // 需要添加的集合为空,直接返回
    return false;

  Node<E> pred, succ;
  if (index == size) { // 插入位置与当前列表数量相同,表示为尾部插入
    succ = null;
    pred = last;
  } else { // 否则,寻找 index 所在节点
    // 找到 index 所在位置的节点,
    // 也就是插入集合后的后一个节点
    succ = node(index);
    // index 所在位置的前一个节点,
    // 也就是插入集合后的前一个节点
    pred = succ.prev;
  }

  // 遍历需要添加的集合,逐个插入
  for (Object o : a) {
    // 创建一个新的节点,以 pred 为前一个节点,值为 e,null 为后一个节点
    @SuppressWarnings("unchecked") E e = (E) o;
    Node<E> newNode = new Node<>(pred, e, null);
    if (pred == null) // 如果 pred 为空,说明是在头部插入
      first = newNode; // 也就是说新建的节点是第一个节点
    else // pred 不为空,说明是在中间或者尾部从插入
      pred.next = newNode; // pred 的下一个节点连接上新创建的节点
    pred = newNode; // 依次插入
  }

  if (succ == null) { // 如果 succ 为空,说明是在尾部插入
    last = pred; // 所以最后插入的元素就是最后一个元素
  } else { // succ 不为空,说明是在中间插入
    pred.next = succ; // 最后插入的元素连接上后面的一段
    succ.prev = pred; // 后面的一段第一个元素连接上前面的一段
  }

  size += numNew; // 数量合并
  modCount++;
  return true;
}

我们来看下 checkPositionIndex() 方法:

private void checkPositionIndex(int index) {
  if (!isPositionIndex(index))
    throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

private boolean isPositionIndex(int index) {
  return index >= 0 && index <= size;
}

看到了很熟悉的异常 IndexOutOfBoundsException,就是根据链表大小检查一下,逻辑很简单。

add()

我们继续分析 add() 方法:

public boolean add(E e) {
  linkLast(e);
  return true;
}

public void add(int index, E element) {
  // 检查 index 是否越界,上面分析过了
  checkPositionIndex(index);

  if (index == size) // 插入位置与当前数量相同,说明是尾部插入
    linkLast(element);
  else
    linkBefore(element, node(index));
}

public void addFirst(E e) {
  linkFirst(e);
}

public void addLast(E e) {
  linkLast(e);
}

这里主要是调用了 linkFirst()linkLast()linkBefore 三个方法,我们继续分析。

private void linkFirst(E e) {
  final Node<E> f = first; // 当前第一个节点
  // 创建了一个新节点,以 null 为前一个节点、e 为值、当前第一个节点为下一个节点
  final Node<E> newNode = new Node<>(null, e, f);
  first = newNode; // 设置新建的节点为第一个节点
  if (f == null) // 当前第一个节点为空,说明列表为空
    last = newNode; // 所以最后一个节点为当前插入的节点
  else // 当前第一个节点不为空,说明列表不为空
    f.prev = newNode; // 当前列表头部连接上插入的节点
  size++;
  modCount++;
}

linkFirst() 逻辑很简单,就是头部插入节点的操作。

void linkLast(E e) {
  final Node<E> l = last;
  final Node<E> newNode = new Node<>(l, e, null);
  last = newNode;
  if (l == null)
    first = newNode;
  else
    l.next = newNode;
  size++;
  modCount++;
}

linkLast() 的逻辑与 linkFirst() 的逻辑相似这里不再分析,大家自己分析一下。

void linkBefore(E e, Node<E> succ) {
  // assert succ != null;
  final Node<E> pred = succ.prev; // 获取到 succ 的上一个节点
  // 创建一个新的节点,连接到 succ 上一个节点后面
  final Node<E> newNode = new Node<>(pred, e, succ);
  succ.prev = newNode; // 将 succ 连接到 newNode 后面
  if (pred == null) // 如果 succ 的上一个节点为空,说明 succ 为头部节点
    first = newNode; // 直接将 newNode 设为头部节点
  else // 如果 succ 的上一个节点不为空,说明 succ 为中间或者尾部节点
    pred.next = newNode; // 将 succ 的上一个节点关联到 newNode 上
  size++;
  modCount++;
}

linkBefore() 的逻辑也很简单,就是在某个节点前面插入一个节点。

get()

我们接着来看 get() 方法。

public E get(int index) {
  // 检查 index 是否越界,上面分析过了
  checkElementIndex(index);
  return node(index).item;
}

get() 中调用了 node() 方法。

Node<E> node(int index) {
  // 如果 index 小于 size 的一半,从开头开始查找 
  if (index < (size >> 1)) {
    Node<E> x = first;
    // 抽头开始查找,直到 i == index
    for (int i = 0; i < index; i++)
      x = x.next;
    return x;
  } else { // 如果 index 大于 size 的一半,从尾部开始查找
    Node<E> x = last;
    for (int i = size - 1; i > index; i--)
      x = x.prev;
    return x;
  }
}

node() 方法的就是根据 index 查找到对应节点,这里用到了折半查找,算是一个小优化。大家可以思考一下这里为什么只折半了一次,而不是一直折半下去呢?

public E getFirst() {
  final Node<E> f = first;
  if (f == null)
    throw new NoSuchElementException();
  return f.item;
}

public E getLast() {
  final Node<E> l = last;
  if (l == null)
    throw new NoSuchElementException();
  return l.item;
}

另外两个 getFirst()getLast() 方法比较简单,大家看一下就行,不再分析。

remove()

我们继续分析 remove() 方法。

public E remove(int index) {
  // 检查 index 是否越界,上面分析过了
  checkElementIndex(index);
  return unlink(node(index));
}

public E removeFirst() {
  final Node<E> f = first;
  if (f == null)
    throw new NoSuchElementException();
  return unlinkFirst(f);
}

public E removeLast() {
  final Node<E> l = last;
  if (l == null)
    throw new NoSuchElementException();
  return unlinkLast(l);
}

可以看到 remove() 方法中分别调用了 unlink()unlinkFirst()unlinkLast() 我们分别来看下。

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; // 当前节点元素设置为空,方便 GC
  size--;
  modCount++;
  return element;
}

可以看到 unlink() 方法就是移除链表上的某个节点。

private E unlinkFirst(Node<E> f) {
  // assert f == first && f != null;
  final E element = f.item; // 获取到当前节点的元素
  final Node<E> next = f.next; // 获取到当前节点的下一个元素
  f.item = null;
  f.next = null; // help GC
  first = next; // 将当前节点的下一个节点设置为头部节点
  if (next == null) // 如果下一个节点为空,说明链表只有一个节点
    last = null; // 清空尾部节点
  else // 否则,说明还有其他节点
    // 下一个节点已经设置为头部节点了
    // 所以清空一下与前一个节点的 关联
    next.prev = null; 
  size--; // 数量 -1
  modCount++;
  return element;
}

unlinkFirst() 方法主要是移除头部节点的操作。

private E unlinkLast(Node<E> l) {
  // assert l == last && l != null;
  final E element = l.item;
  final Node<E> prev = l.prev;
  l.item = null;
  l.prev = null; // help GC
  last = prev;
  if (prev == null)
    first = null;
  else
    prev.next = null;
  size--;
  modCount++;
  return element;
}

unlinkLast() 方法与 unlinkFirst() 方法的逻辑相似,这里不再赘述,大家自己分析下就好。

clear()

我们最后分析下 clear() 方法:

public void clear() {
  // 遍历一遍全部设置为空
  for (Node<E> x = first; x != null; ) {
    Node<E> next = x.next;
    x.item = null;
    x.next = null;
    x.prev = null;
    x = next;
  }
  first = last = null;
  size = 0;
  modCount++;
}

可以看到 clear() 方法操作很简单,就是遍历一下全部设置为空。

总结

通过上面的分析,大家对 LinkedList 的底层实现应该很清楚了。总体来看 LinkedList 的源码并不复杂,大家耐心看一下,一般都能看懂。好了本文就到这里了,希望对大家有所帮助。

1

评论区