LinkedList源码解析及常用方法

LinkedList源码解析及常用方法

LinkedList简介

首先,我们先通过LinkedList注释来了解这个集合的性质。

双链表实现了list和Deque接口。实现所有可选的列表操作,并允许所有元素(包括null)。所有操作的执行都与双链表所预期的一样。索引到列表中的操作将从开始或结束遍历列表,以更接近指定索引的为准。请注意,此实现不是同步的。如果多个线程同时访问一个链表,并且至少有一个线程在结构上修改了该列表,则必须对其进行外部同步。(结构修改是指添加或删除一个或多个元素的任何操作;仅仅设置元素的值并不是结构修改。)这通常是通过对自然封装列表的某个对象进行同步来实现的。如果不存在这样的对象,则应使用Collections.synchronizedList方法“包装”列表。这最好在创建时完成,以防止意外地对列表进行不同步的访问:list list=Collections.synchronizedList(new LinkedList(…));此类迭代器和listIterator方法返回的迭代器是故障快速的:如果在迭代器创建后的任何时候,以除迭代器自己的remove或add方法之外的任何方式对列表进行结构修改,则迭代器将抛出ConcurrentModificationException。因此,面对并发修改,迭代器会快速而干净地失败,而不是冒着在未来不确定的时间出现任意、不确定行为的风险。请注意,迭代器的故障快速行为无法得到保证,因为一般来说,在存在不同步的并发修改的情况下,不可能做出任何硬保证。故障快速迭代器在尽力而为的基础上抛出ConcurrentModificationException。因此,编写依赖于此异常的正确性的程序是错误的:迭代器的故障快速行为应该只用于检测错误。这个类是Java集合框架的成员。

总结上述我们可以发现LinkedList链表的特点:

  • 底层是一个双向链表结构:增删快,查询慢
    在这里插入图片描述

  • 包含大量操作首尾元素的方法

  • 线程不安全

下面我们看一下ArrayList的族谱!
在这里插入图片描述

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()从这个列表所表示的堆栈中弹出一个元素。换言之,移除并返回此列表的第一个元素。

源码解析

这里以 JDK1.8 为例,分析一下 LinkedList 常用方法的底层核心源码。

LinkedList 中的元素是通过Node定义的:

private static class Node<E> {
    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 中有一个无参构造函数和一个有参构造函数。

// 创建一个空的链表对象
public LinkedList() {
}

// 接收一个集合类型作为参数,会创建一个与传入集合相同元素的链表对象
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

插入元素

LinkedList 除了实现了List接口相关方法,还实现了 Deque 接口的很多方法,所以我们有很多种方式插入元素。我们这里以List接口中相关的插入方法为例进行源码讲解,对应的是add() 方法。
add() 方法有两个版本:

  • add(E e):用于在 LinkedList 的尾部插入元素,即将新元素作为链表的最后一个元素,时间复杂度为 O(1)。
  • add(int index, E element):用于在指定位置插入元素。这种插入方式需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。
// 在链表尾部插入元素
public boolean add(E e) {
    linkLast(e);
    return true;
}

// 在链表指定位置插入元素
public void add(int index, E element) {
    // 下标越界检查
    checkPositionIndex(index);

    // 判断 index 是不是链表尾部位置
    if (index == size)
        // 如果是就直接调用 linkLast 方法将元素节点插入链表尾部即可
        linkLast(element);
    else
        // 如果不是则调用 linkBefore 方法将其插入指定元素之前
        linkBefore(element, node(index));
}

// 将元素节点插入到链表尾部
void linkLast(E e) {
    // 将最后一个元素赋值(引用传递)给节点 l
    final Node<E> l = last;
    // 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空
    final Node<E> newNode = new Node<>(l, e, null);
    // 将 last 引用指向新节点
    last = newNode;
    // 判断尾节点是否为空
    // 如果 l 是null 意味着这是第一次添加元素
    if (l == null)
        // 如果是第一次添加,将first赋值为新节点,此时链表只有一个元素
        first = newNode;
    else
        // 如果不是第一次添加,将新节点赋值给l(添加前的最后一个元素)的next
        l.next = newNode;
    size++;
    modCount++;
}

// 在指定元素之前插入元素
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;断言 succ不为 null
    // 定义一个节点元素保存 succ 的 prev 引用,也就是它的前一节点信息
    final Node<E> pred = succ.prev;
    // 初始化节点,并指明前驱和后继节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    // 将 succ 节点前驱引用 prev 指向新节点
    succ.prev = newNode;
    // 判断尾节点是否为空,为空表示当前链表还没有节点
    if (pred == null)
        first = newNode;
    else
        // succ 节点前驱的后继引用指向新节点
        pred.next = newNode;
    size++;
    modCount++;
}

获取元素

LinkedList获取元素相关的方法一共有 3 个:

  • getFirst():获取链表的第一个元素。
  • getLast():获取链表的最后一个元素。
  • get(int 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;
}

// 获取链表指定位置的元素
public E get(int index) {
  // 下标检查
  checkElementIndex(index);
  // 返回链表中对应下标的元素
  return node(index).item;
}

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

// 返回指定下标的非空节点
Node<E> node(int index) {
    // 断言下标未越界
    // assert isElementIndex(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 {
        Node<E> x = last;
        //遍历,循环向前查找,直至 i == index
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

get(int index)remove(int index)等方法内部都调用了该方法来获取对应的节点。
不难看出,该方法通过比较索引值与链表 size 的一半大小来确定从链表头还是链表尾开始遍历。

  • 如果索引值小于 size 的一半,就从链表头开始遍历
  • 如果索引值大于 size 的一半,就从链表尾开始遍历。

这样可以在较短的时间内找到目标节点,充分利用了双向链表的特性来提高效率。

删除元素

LinkedList删除元素相关的方法一共有 5 个:

  • removeFirst():删除并返回链表的第一个元素。
  • removeLast():删除并返回链表的最后一个元素。
  • remove(E e):删除链表中首次出现的指定元素,如果不存在该元素则返回 false。
  • remove(int index):删除指定索引处的元素,并返回该元素的值。
  • void clear():移除此链表中的所有元素。
// 删除并返回链表的第一个元素
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);
}

// 删除链表中首次出现的指定元素,如果不存在该元素则返回 fals
public boolean remove(Object o) {
    // 如果指定元素为 null,遍历链表找到第一个为 null 的元素进行删除
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        // 如果不为 null ,遍历链表找到要删除的节点
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}

// 删除链表指定位置的元素
public E remove(int index) {
    // 下标越界检查,如果越界就抛异常
    checkElementIndex(index);
    return unlink(node(index));
}

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

E unlink(Node<E> x) {
    // 断言 x 不为 null
    // 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 { // 如果前一个节点不为空
        // 将前一个节点的 next 指针指向当前节点的下一个节点
        prev.next = next;
        // 将当前节点的 prev 指针置为 null,,方便 GC 回收
        x.prev = null;
    }

    // 如果下一个节点为空,则说明当前节点是尾节点
    if (next == null) {
        // 直接让链表尾指向当前节点的前一个节点
        last = prev;
    } else { // 如果下一个节点不为空
        // 将下一个节点的 prev 指针指向当前节点的前一个节点
        next.prev = prev;
        // 将当前节点的 next 指针置为 null,方便 GC 回收
        x.next = null;
    }

    // 将当前节点元素置为 null,方便 GC 回收
    x.item = null;
    size--;
    modCount++;
    return element;
}

unlink() 方法的逻辑如下:

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

遍历链表

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

LinkedList<String> list = new LinkedList<>();
list.add("apple");
list.add("banana");
list.add("pear");

for (String fruit : list) {
    System.out.println(fruit);
}

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

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

下面我们对迭代器 ListItr 中的核心方法进行详细介绍。
根据方向来分有两种:

  • 头到尾方向的迭代
// 判断还有没有下一个节点
public boolean hasNext() {
    // 判断下一个节点的下标是否小于链表的大小,如果是则表示还有下一个元素可以遍历
    return nextIndex < size;
}
// 获取下一个节点
public E next() {
    // 检查在迭代过程中链表是否被修改过
    checkForComodification();
    // 判断是否还有下一个节点可以遍历,如果没有则抛出 NoSuchElementException 异常
    if (!hasNext())
        throw new NoSuchElementException();
    // 将 lastReturned 指向当前节点
    lastReturned = next;
    // 将 next 指向下一个节点
    next = next.next;
    nextIndex++;
    return lastReturned.item;
}
  • 尾到头方向的迭代
// 判断是否还有前一个节点
public boolean hasPrevious() {
    return nextIndex > 0;
}

// 获取前一个节点
public E previous() {
    // 检查是否在迭代过程中链表被修改
    checkForComodification();
    // 如果没有前一个节点,则抛出异常
    if (!hasPrevious())
        throw new NoSuchElementException();
    // 将 lastReturned 和 next 指针指向上一个节点
    lastReturned = next = (next == null) ? last : next.prev;
    nextIndex--;
    return lastReturned.item;
}

当然迭代器还有其他的用途:

  • add()插入元素
LinkedList<String> list = new LinkedList<>();
list.add("apple");
list.add(null);
list.add("banana");

//  Collection 接口的 removeIf 方法底层依然是基于迭代器
list.removeIf(Objects::isNull);

for (String fruit : list) {
    System.out.println(fruit);
}
  • remove()移除元素
// 从列表中删除上次被返回的元素
public void remove() {
    // 检查是否在迭代过程中链表被修改
    checkForComodification();
    // 如果上次返回的节点为空,则抛出异常
    if (lastReturned == null)
        throw new IllegalStateException();

    // 获取当前节点的下一个节点
    Node<E> lastNext = lastReturned.next;
    // 从链表中删除上次返回的节点
    unlink(lastReturned);
    // 修改指针
    if (next == lastReturned)
        next = lastNext;
    else
        nextIndex--;
    // 将上次返回的节点引用置为 null,方便 GC 回收
    lastReturned = null;
    expectedModCount++;
}

总结

LinkedList底层是链表

  • 查询慢、增删快
  • 存储空间不连续,需要通过指针关联,在查询过程中需要不断跳转新的地址

LinkedList是链表操作:

  • add(E e):在数组尾部添加元素,时间复杂度为O(1)
  • add(int index, E element):在索引为index的位置添加元素,指针指向操作,时间复杂度为O(1)
  • remove(int index)/remove(Object o):删除元素,指针指向操作,时间复杂度为O(1)
  • set(int index, E element):修改元素,时间复杂度为O(n)
  • get(int index):获取索引为index的元素,时间复杂度为O(n)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值