Java集合——LinkedList源码分析

时间:2021-05-08 19:34:53

一 前言

上一篇我们介绍了ArrayList源码解析有想看的同学可以点击这个链接ArrayList源码解析。平时我们或多或少都用过LinKedList,但是对其原理不是很了解,我们就来一起学习吧。

二 源码解析

1. LinkedList概述

LinkedList是一个实现了List接口和Deque接口的双端链表。
有关索引的操作可能从链表头开始遍历到链表尾部,也可能从尾部遍历到链表头部,这取决于看索引更靠近哪一端。
LinkedList不是线程安全的,如果想使LinkedList变成线程安全的,可以使用如下方式:

List list=Collections.synchronizedList(new LinkedList(...));
  1. LinkedList底层是双向链表存储数据,并且记录了头节点和尾节点
  2. 插入和删除比较快(O(1)),查询则相对慢一些(O(n))。
  3. 删除也是非常快,只需要改动一下指针就行了,代价很小.
  4. 添加元素非常快,如果是添加到头部和尾部的话更快,因为已经记录了头节点和尾节点,只需要链接一下就行了. 如果是添加到链表的中间部分的话,那么多一步操作,需要先找到添加索引处的元素(因为需要链接到这里),才能进行添加.
  5. 因为是链表结构,所以分配的空间不要求是连续的
  6. 线程不安全
  7. iterator()和listIterator()返回的迭代器都遵循fail-fast机制。
  8. 遍历的时候,建议采用forEach()进行遍历,这样可以在每次获取下一个元素时都非常轻松(next = next.next;). 然后如果是通过fori和get(i)的方式进行遍历的话,效率是极低的,每次get(i)都需要从最前面(或者最后面)开始往后查找i索引处的元素,效率很低.

LinkedList底层是链表结构,说具体点他是双向循环链表。什么是双向链表呢?
Java集合——LinkedList源码分析

双向链表的每个节点包含以下数据:上一个节点的指针,自己的数据,下一个节点的指针.尾节点没有下一个节点,所以指向null.这样的结构,比如我拿到链表中间的一个节点,即可以往前遍历,也可以往后遍历.

2 LinkedList继承关系

先上一盘菜

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

Java集合——LinkedList源码分析

Java集合——LinkedList源码分析

  1. LinkedList继承于AbstractSequentialList,AbstractSequentialList这个类提供了List的一个骨架实现接口,以尽量减少实现此接口所需的工作量由“顺序访问”数据存储(如链接列表)支持。对于随机访问数据(如数组),应使用AbstractList优先于此类。
  2. LinkedList 实现了List接口,意味着LinkedList元素是有序的,可以重复的,可以有null元素的集合。
  3. LinkedList 实现 Deque 接口,Deque是Queue的子接口,Queue是一种队列形式,而Deque是双向队列,它支持从两个端点方向检索和插入元素。
  4. LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆,可以被复制.注意,LinkedList里面的clone()复制其实是浅复制。
  5. LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输。

3 LinkerList 全局变量

LinkedList本身的的属性比较少,主要有三个:

  1. size 当前有多少个节点。
  2. first 代表第一个节点。
  3. last 代表最后一个节点。
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{   
    //当前有多少个节点
    transient int size = 0;
    //第一个节点
    transient Node<E> first;
    //最后一个节点
    transient Node<E> last;
}

这个 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;
    }
}

Node是链表的节点,Node是LinkedList的静态内部类,数据结构也比较简单,如下:
1. item 该节点的数据。
2. next 指向下一个节点的指针。
3. prev 指向上一个节点的指针。

4 构造方法

/** * 构造一个空列表 */
public LinkedList() {
}

/** * 构造列表通过指定的集合 */
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

LinkedList一共有2个构造方法,第一个构造一个空列表,第二个构造列表通过指定的集合。我们主要看第二个,

//将指定集合的所有元素插入到末尾位置
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);
}

//将指定集合的所有元素插入到index位置
public boolean addAll(int index, Collection<? extends E> c) {
    //1. 入参合法性检查
    checkPositionIndex(index);

    //2. 将集合转成数组
    Object[] a = c.toArray();
    //3. 记录需要插入的集合元素个数
    int numNew = a.length;
    //4. 如果个数为0,那么插入失败,不继续执行了
    if (numNew == 0)
        return false;

    //5. 判断一下index与size是否相等
    //相等则插入到链表末尾
    //不相等则插入到链表中间 index处 
    Node<E> pred, succ;   
    if (index == size) {
        succ = null;
        pred = last;
    } else {
        //找到index索引处节点 这样就可以方便的拿到该节点的前后节点信息
        succ = node(index);
        //记录index索引处节点前一个节点
        pred = succ.prev;
    }

    //6. 循环将集合中所有元素连接到pred后面
    for (Object o : a) {
        @SuppressWarnings("unchecked") E e = (E) o;
        Node<E> newNode = new Node<>(pred, e, null);
        //如果前一个是空,那么将新节点作为头结点
        if (pred == null)
            first = newNode;
        else
            //指向新节点
            pred.next = newNode;
        pred = newNode;
    }

    //7. 判断succ是否为空
    //为空的话,那么集合的最后一个元素就是尾节点
    //非空的话,那么将succ连接到集合的最后一个元素后面
    if (succ == null) {
        last = pred;
    } else {
        pred.next = succ;
        succ.prev = pred;
    }

    //8. 链表长度+numNew
    size += numNew;
    modCount++;
    return true;
}

解析:addAll 这段代码分为了2种情况,一个是原来的链表是空的,一个是原来的链表有值。我们这边是构造方法是第一种情况。

  1. 将需要添加的集合转成数组a。
  2. 判断需要插入的位置index是否等于链表长度size,如果相等则插入到链表最后;如果不相等,则插入到链表中间,还需要找到index处节点succ,方便拿到该节点的前后节点信息.
  3. 记录index索引处节点的前一个节点pred,循环将集合中所有元素连接到pred的后面
  4. 将集合最后一个元素的next指针指向succ,将succ的prev指针指向集合的最后一个元素

5 添加元素各种方法

5.1 add(E e)
/** * 添加指定元素到链表尾部 */
public boolean add(E e) {
    linkLast(e);
    return true;
}
/** * Links e as last element.将e添加到尾部 */
void linkLast(E e) {
    //1. 暂记尾节点
    final Node<E> l = last;
    //2. 构建节点 前一个节点是之前的尾节点
    final Node<E> newNode = new Node<>(l, e, null);
    //3. 新建的节点是尾节点了
    last = newNode;
    //4. 判断之前链表是否为空 
    //为空则将新节点赋给头结点(相当于空链表插入第一个元素,头结点等于尾节点)
    //非空则将之前的尾节点指向新节点
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    //5. 链表长度增加
    size++;
    modCount++;
}

注意点:
boolean add(E e) 添加成功返回true,添加失败返回false.我们在代码中没有看到有返回false的情况啊,直接在代码中写了个返回true,什么判断条件都没有,之前我们说过链表的数据存储不需要连续的空间存储,所以只要是还能给它分配空间,就不会添加失败.当空间不够分配时(内存溢出),会抛出OutOfMemory。

5.2 addLast(E e)
//添加元素到末尾. 内部实现和add(E e)一样
public void addLast(E e) {
    linkLast(e);
}
5.3 addFirst(E e)
public void addFirst(E e) {
    linkFirst(e);
}
/** 1. 添加元素到链表头部 */
private void linkFirst(E e) {
    //1. 记录头结点
    final Node<E> f = first;
    //2. 创建新节点 next指针指向之前的头结点
    final Node<E> newNode = new Node<>(null, e, f);
    //3. 新建的节点就是头节点了
    first = newNode;
    //4. 判断之前链表是否为空 
    //为空则将新节点赋给尾节点(相当于空链表插入第一个元素,头结点等于尾节点)
    //非空则将之前的头结点的prev指针指向新节点
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    //5. 链表长度增加
    size++;
    modCount++;
}

解析:
1. 记录头结点
2. 构建一个新的节点
3. 将该新节点作为新的头节点.如果是空链表插入第一个元素,那么头结点= 尾节点=新节点;如果不是,那么将之前的头节点的prev指针指向新节点.
4. 增加链表长度
5. 列表内容

5.4 push(E e)
public void push(E e) {
    addFirst(e);
}

添加元素到链表头部 这里的意思比拟压栈.和pop(出栈:移除链表第一个元素)相反.
还记得LinkedList继承关系吗?
LinkedList 实现 Deque 接口,push(E e)就是 Deque 接口中的方法。
push(E e)内部实现是和addFirst()一样的。

5.5 offer(),offerFirst(E e),offerLast(E e)
public boolean offer(E e) {
    return add(e);
}
public boolean offerFirst(E e) {
    addFirst(e);
    return true;
}

/** * 添加元素到末尾 */
public boolean offerLast(E e) {
    addLast(e);
    return true;
}

添加元素到链表头部. 内部实现其实就是add(e)

5.6 add(int index, E element)
//添加元素到指定位置
public void add(int index, E element) {
    //1. 越界检查
    checkPositionIndex(index);

    //2. 判断一下index大小
    //如果是和list大小一样,那么就插入到最后
    //否则插入到index处
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

//检查是否越界
private void checkPositionIndex(int index) {
    if (!isPositionIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

/** * Returns the (non-null) Node at the specified element index. 返回指定元素索引处的(非空)节点。 */
Node<E> node(int index) {
    // assert isElementIndex(index);

    /** * 这里的思想非常巧妙,如果index在链表的前半部分,那么从first开始往后查找 否则,从last往前面查找,节省查找时间 */
    //1. 如果index<size/2 ,即index在链表的前半部分
    if (index < (size >> 1)) {
        //2. 记录下第一个节点
        Node<E> x = first;
        //3. 循环从第一个节点开始往后查,直到到达index处,返回index处的元素
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        //index在链表的后半部分
        //4. 记录下最后一个节点
        Node<E> x = last;
        //5. 循环从最后一个节点开始往前查,直到到达index处,返回index处的元素
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}
/** * Links e as last element. 将e链接到list最后一个元素 */
void linkLast(E e) {
    //1. 记录最后一个元素l
    final Node<E> l = last;
    //2. 构建一个新节点,数据为e,前一个是l,后一个是null
    final Node<E> newNode = new Node<>(l, e, null);
    //3. 现在新节点是最后一个元素了,所以需要记录下来
    last = newNode;
    //4. 如果之前list为空,那么first=last=newNode,只有一个元素
    if (l == null)
        first = newNode;
    else
        //5. 非空的话,那么将之前的最后一个指向新的节点
        l.next = newNode;
    //6. 链表长度+1
    size++;
    modCount++;
}

/** * Inserts element e before non-null Node succ. 在非null节点succ之前插入元素e。 */
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    //1. 记录succ的前一个节点
    final Node<E> pred = succ.prev;
    //2. 构建一个新节点,数据是e,前一个节点是pred,下一个节点是succ
    final Node<E> newNode = new Node<>(pred, e, succ);
    //3. 将新节点作为succ的前一个节点
    succ.prev = newNode;
    //4. 判断pred是否为空
    //如果为空,那么说明succ是之前的头节点,现在新节点在succ的前面,所以新节点是头节点
    if (pred == null)
        first = newNode;
    else
        //5. succ的前一个节点不是空的话,那么直接将succ的前一个节点指向新节点就可以了
        pred.next = newNode;
    //6. 链表长度+1
    size++;
    modCount++;
}

6 删除元素的各种方法

6.1 remove() removeFirst()

移除链表第一个元素

/** * 移除链表第一个节点 */
public E remove() {
    return removeFirst();
}

/** * 移除链表第一个节点 */
public E removeFirst() {
    final Node<E> f = first;
    //注意:如果之前是空链表,移除是要报错的哟
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

/** * Unlinks non-null first node f. * 将第一个节点删掉 */
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    //1. 记录第一个节点的数据值
    final E element = f.item;
    //2. 记录下一个节点
    final Node<E> next = f.next;
    //3. 将第一个节点置空 帮助GC回收
    f.item = null;
    f.next = null; // help GC
    //4. 记录头节点
    first = next;
    //5. 如果下一个节点为空,那么链表无节点了 如果不为空,将头节点的prev指针置为空
    if (next == null)
        last = null;
    else
        next.prev = null;
    //6. 链表长度-1
    size--;
    modCount++;
    //7. 返回删除的节点的数据值
    return element;
}

解析:将第一个节点移除并置空,然后将第二个节点作为头节点.思路还是非常清晰的,主要是对细节的处理.

6.2 remove(int index)

移除指定位置元素

//移除指定位置元素
public E remove(int index) {
    //检查入参是否合法
    checkElementIndex(index);
    //node(index)找到index处的节点 
    return unlink(node(index));
}

//移除节点x
E unlink(Node<E> x) {
    // assert x != null;
    //1. 记录该节点数据值,前一个节点prev,后一个节点next
    final E element = x.item;
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    //2. 判断前一个节点是否为空
    if (prev == null) {
        //为空的话,那么说明之前x节点是头节点 这时x的下一个节点成为头节点
        first = next;
    } else {
        //非空的话,将前一个节点的next指针指向x的下一个节点
        prev.next = next;
        //x的prev置为null
        x.prev = null;
    }

    //3. 判断x后一个节点是否为空
    if (next == null) {
        //为空的话,那么说明之前x节点是尾节点,这时x的前一个节点成为尾节点
        last = prev;
    } else {
        //为空的话,将x的下一个节点的prev指针指向prev(x的前一个节点)
        next.prev = prev;
        //x的next指针置空
        x.next = null;
    }

    //4. x节点数据值置空
    x.item = null;
    //5. 链表长度-1
    size--;
    modCount++;
    //6. 将x节点的数据值返回
    return element;
}

解析:

  1. 首先找到index索引处的节点(这样就可以方便的获取该节点的前后节点),记为x 。
  2. 记录x的前(prev)后(next)节点 。
  3. 将x的前一个节点prev节点的next指针指向next,将x节点的后一个节点的
    prev指针指向prev节点。
  4. 将x节点置空,链表长度-1。
6.3 remove(Object o) removeFirstOccurrence(Object o)

从此链表中删除第一次出现的指定元素o

public boolean remove(Object o) {
    //1. 判断o是否为空
    if (o == null) {
        //为null 循环,找第一个数据值为null的节点
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                //删除该节点
                unlink(x);
                return true;
            }
        }
    } else {
        //非空 循环,找第一个与o的数据值相等的节点
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                //删除该节点
                unlink(x);
                return true;
            }
        }
    }
    return false;
}
//从此链表中删除第一次出现的指定元素o. 内部其实就是上面的remove(o);
public boolean removeFirstOccurrence(Object o) {
    return remove(o);
}

解析:
1. 首先判断入参是否为null
2. 如果为null,那么循环遍历链表,从头节点开始往后查找,找到第一个节点的数据值为null的,直接删除该节点.
3. 如果非null,那么循环遍历链表,从头节点开始往后查找,找到第一个节点的数据值为o的,直接删除该节点.

6.4 removeLast()

移除最后一个元素并返回

public E removeLast() {
    final Node<E> l = last;
    //如果链表是空的,那么就要抛出一个错误
    if (l == null)
        throw new NoSuchElementException();
    return unlinkLast(l);
}
/** 1. Unlinks non-null last node l. 移除链表最后一个元素 */
private E unlinkLast(Node<E> l) {
    // assert l == last && l != null;

    //1. 记录尾节点数据值
    final E element = l.item;
    //2. 找到尾节点的前一个节点prev
    final Node<E> prev = l.prev;
    //3. 将尾节点置空 方便GC
    l.item = null;
    l.prev = null; // help GC
    //4. 将last赋值为prev 
    last = prev;
    //5. 判断prev是否为null
    //为空的话,说明之前链表就只有1个节点,现在删了之后,头节点和尾节点都为null了
    //非空,直接将新任尾节点的next指针指向null
    if (prev == null)
        first = null;
    else
        prev.next = null;
    //6. 链表长度-1
    size--;
    modCount++;
    //7. 返回之前尾节点数据值
    return element;
}

解析:
1. 判断链表是否有节点, 没有节点直接抛错误….
2. 首先找到倒数第二个节点(可能没有哈,没有的话,说明链表只有一个节点)prev
3. 然后将尾节点置空,prev的next指针指向null
4. 链表长度-1, 返回之前尾节点数据值

6.5 removeLastOccurrence(Object o)

从此链表中删除最后一次出现的指定元素o.其实和上面的remove(o)是一样的,只不过这里遍历时是从尾节点开始往前查找的.

public boolean removeLastOccurrence(Object o) {
    if (o == null) {
       //为null 循环,从后向前 找第一个数据值为null的节点
        for (Node<E> x = last; x != null; x = x.prev) {
            if (x.item == null) {
                //删除该节点
                unlink(x);
                return true;
            }
        }
    } else {
        //不为null 循环,从后向前 找第一个数据值为null的节点
        for (Node<E> x = last; x != null; x = x.prev) {
            if (o.equals(x.item)) {
                //删除该节点
                unlink(x);
                return true;
            }
        }
    }
    return false;
}
6.6 poll() pop()
//获取第一个元素的同时删除第一个元素,当链表无节点时,不会报错. 这里的unlinkFirst()上面已分析过.
public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

//获取第一个元素的同时删除第一个元素,当链表无节点时,会报错.
public E pop() {
    return removeFirst();
}
public E removeFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
7 修改元素 set(int index, E element)

首先找到index处节点,替换该节点数据值

设置index处节点数据值为element
public E set(int index, E element) {
    //1. 入参检测
    checkElementIndex(index);
    //2. 找到index处节点,上面已分析该方法
    Node<E> x = node(index);
    //3. 保存该节点旧值
    E oldVal = x.item;
    //4. 替换为新值
    x.item = element;
    //5. 将旧值返回
    return oldVal;
}
8 查询元素
8.1 element() getFirst()
//获取链表第一个元素. 方法比较简单,就是将链表头节点数据值进行返回
public E element() {
    return getFirst();
}
/ 获取链表第一个元素. 非常简单,就是将first的数据值返回
public E getFirst() {
    final Node<E> f = first;
    if (f == null)
        throw new NoSuchElementException();
    return f.item;
}
8.2 get(int index)
//获取指定索引处元素. 方法比较简单,就是通过node(index)找到index索引处节点,然后返回其数据值
public E get(int index) {
    //1. 入参检测
    checkElementIndex(index);
    //2. 获取指定索引处节点数据值
    return node(index).item;
}
8.3 getLast()
//获取链表最后一个元素. 非常简单,就是将last的数据值返回
public E getLast() {
    final Node<E> l = last;
    if (l == null)
        throw new NoSuchElementException();
    return l.item;
}