Java常用集合框架——LinkedList源码分析

本文详细解析了LinkedList的内部结构、实现原理及各种操作方法,包括插入、删除、遍历等,并介绍了其队列和双向队列特性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LinkedList(链表数组)

目录:

1、UML结构图
2、源码分析
3、队列特性
4、双向队列特性
5、迭代器
6、剩余的方法
7、总结
个人理解

1、首先查看一下它的UML结构图:

LinkedList的UML关系结构

  • Cloneable 和 Serializable都是功能性接口,前者是使得该链表可以被序列化,后者则是可以实现复制
  • 主要看DeuqeAbstractSequentialList
    • 前者是一个接口,实现它使得链表可以当做双端队列使用;
    • 后者是一个抽象类,这个类声明此链表具有随机访问的特性;
  • 同时由该UML结构图可以看出,该类并没有实现RandomAccess接口,说明遍历该类使用增强型循环或者是迭代器的形式做遍历效果会更佳;

2、看完分析完UML结构图之后就进入正题,开始分析LinkedList的具体实现了

1)先看几个成员属性
//transient关键字表名这三个属性是不能被序列化的
transient int size = 0;   //记录LinkedList的大小

transient Node<E> first;  //头节点

transient Node<E> last;   //尾节点

//Node的具体实现,作为LinkedList底层的数据结构支持
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;
    }
}
2)提供的构造器
  • 无参构造
  • 有参构造:给定一个包含若干元素的集合,构造完成之后链表即具有集合大小的长度
public LinkedList() {}  //无参构造器

//给定含有一定元素的集合
public LinkedList(Collection<? extends E> c) {
    this();  //调用无参构造器完成初始化
    addAll(c); //调用该方法将给定集合中的元素保存在LinkedList中
}

//顺带看看 addAll方法
public boolean addAll(Collection<? extends E> c) {
    return addAll(size, c);  //老套娃了
}

//真正执行批量插入操作的方法体
public boolean addAll(int index, Collection<? extends E> c) {
    checkPositionIndex(index);  //首先判断开始插入的索引是否合法,否则将抛出索引越界异常

    Object[] a = c.toArray();
    int numNew = a.length;
    if (numNew == 0)     //如果给定的集合是空的,返回false
        return false;

    Node<E> pred, succ;   //succ用于保存位于index位置的节点 , pred表示目标位置的前一个节点
    if (index == size) {
        succ = null;
        pred = last;     //如果index等于size表示在链表最后插入
    } else {
        succ = node(index);   //这里是通过node(index)这个方法找到succ的节点
        pred = succ.prev;  
    }
 	
    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;
    }

    if (succ == null) {             //如果目标位置为空
        last = pred;                //尾节点就是最后一个元素
    } else {
        pred.next = succ;          //不然的话将从index位置断开的链表续上
        succ.prev = pred;
    }

    size += numNew;               //修改当前容量
    modCount++;                   //修改次数也+1
    return true;                  //返回结果
}

node(int index)

//通过这个方法寻找index位置的节点
Node<E> node(int index) {
    // assert isElementIndex(index);
    if (index < (size >> 1)) {    //小小的优化,每一次查找最多只用查找链表长度的一半
        Node<E> x = first;        //如果是在前半部分,就从前往后找
        for (int i = 0; i < index; i++)
            x = x.next;          
        return x;
    } else {
        Node<E> x = last;         //如果是在后半部分,就从后往前找
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}
3)链式列表提供的几种add()方法:
//经常使用的add方法
public boolean add(E e) {
    linkLast(e);  //默认是在尾部追加, 下面会对这个方法进行分析
    return true;
}

//向指定位置(index)添加新的元素
public void add(int index, E element) {
    checkPositionIndex(index);

    if (index == size)
        linkLast(element);                  //这里还是调用了尾部追加的方法
    else
        linkBefore(element, node(index));   //又引出一个新的方法,下面一起分析
}

//在尾部追加的方法
void linkLast(E e) {
    final Node<E> l = last;   //这里使用了final修饰符,保证了l在赋值完之后不能够被改变
    final Node<E> newNode = new Node<>(l, e, null);  //这里利用final做同样的事情,在构造newNode的时候就已经是将l与newNode关联起来,此时newNode的pred已经指向了l,即链列表的尾部
    last = newNode;          //newNode作为新的尾巴
    if (l == null)           //如果l是null的,则链式列表是空的,第一个节点也将是newNode
        first = newNode;
    else
        l.next = newNode;    //旧尾巴的next也要指向newNode,确保列表的完整性
    size++;                  //节点数+1
    modCount++;              //修改次数+1
}


//这里比较细节,需要结合着调用位置一起看
//@param succ 是将要插入位置的节点
//@E e 待插入的新值
void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;   //获取待插入位置的前一个节点引用
    // pred <- (前) newNode (后)  -> (succ) 
    final Node<E> newNode = new Node<>(pred, e, succ); 
    //修正原index位置节点的前驱, newNode <- (前) succ (后) -> 原先的序列
    succ.prev = newNode;
    if (pred == null)       //下同linkLast(E e)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

//补充分析一下头插法
private void linkFirst(E e) {
    final Node<E> f = first;    //保留一份头节点的引用
    //完成    null <- (头节点前驱为null,不是循环队列) newNode (后) -> f
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;            //让表示头节点的变量指向newNode
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;      // newNode <- (前) f (后) -> 原先的序列
    size++;
    modCount++;
}
4)用于删除的三个方法:
  • 要注意的点是,这里面有两个方法都是private的,向外界暴露的调用方法如下:
    • 调用unlinkFirst的方法:removeFirst() poll() pollFirst()
    • 调用unlinkLast的方法:removeLast() pollLast()
    • 而调用unlink的方法:removeLastOccurrence(Object o) remove(int index) removeLast(Obeject o)
      • 关于remove的方法:其实两个方法的本质差不多,都是通过调用unlink完成核心删除工作;
      • 如果传递的参数的是index,则需要先调用node(index)方法找到目标节点之后才能够继续完成删除工作;
      • 如果传递的是object参数,则是要判断传递的object是否为null,接着做一些细节上的处理(避免空指针);
      • 而通过查看remove(Object o)的实现细节,可以得知链表是可以保存null值的(这里仅仅是指节点的item可以为null,作为链表的组成部分,每一个Node都不能够为null);
//删除头节点
//当外部调用者是不用传递这个Node参数的,因为这里的f其实first的引用
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;                //主动让Node的item和next这两个变量的指向null,使得下一次GC可以将它俩回收
    f.next = null; // help GC
    first = next;                 //让头节点的下一个节点上位成为新的头节点
    if (next == null)             //以下的代码大多与上面大同小异,道理一样
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;           //这里需要注意的时候返回的是被移除节点中保存着的item对象,下同
}

//删除尾节点
//这里的l也是无须外部调用者传递的,l的引用是指向当前链表的尾节点的
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;
}

//这里是比较常用的
E unlink(Node<E> x) {
    // assert x != null; 
    //常规三部曲:拿到Node的item引用、前驱引用、后继引用
    final E element = x.item;         
    final Node<E> next = x.next;
    final Node<E> prev = x.prev;

    if (prev == null) {  //如果前驱为空,说明当前的node其实是头节点
        first = next;    //更换头节点
    } else {             //不是头节点
        prev.next = next;   //前驱直接指向后继
        x.prev = null;    //前驱置空
    }

    if (next == null) {   //这里做当前节点是不是尾节点的判断工作
        last = prev;      //如果是,则尾节点的引用指向前驱
    } else {              //不是尾节点
        next.prev = prev;  //后继的前驱指向当前节点的前驱
        x.next = null;    //当前的node对象x彻底不可达了,成了“孤魂野鬼”,等待GC回收
    }

    x.item = null;       //节点的item域置空,等待GC回收
    size--;
    modCount++;
    return element;
}
5)其他的一些常用方法:
  • size();时间复杂度为O(1),因为链表提供了一个记录值,当链表发生长度变化的时候,相应的记录值会直接跟着更新,所以直接访问该记录值即可得到结果;
  • node(int index); 查找位于给定位置的节点,并且返回;这里上文也有分析过(点解直达代码处),比较值得留意的是这里所做的优化,最多也是只要遍历整个链表长度的一半节点就可以找到节点;
  • checkPositionIndex(index); 这个私有方法常见于需要传递一个索引的方法中,其会在调用方法执行前先被调用,作用是判断传递的index参数是否合法,而且这个方法也是调用了一个封装的私有方法(isPositionIndex(int index))来工作的,代码只有一行return index >= 0 && index <= size;,如果不是合法的值则将throw new IndexOutOfBoundsException(outOfBoundsMsg(index));越界异常;
  • contains(Object o);indexOf(o),前者的代码里面只是简单的调用了一下后者,那么我们继续分析一下indexOf这个方法:
//获取给定对象在链表中的索引
public int indexOf(Object o) {
    int index = 0;
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null)
                return index;
            index++;
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item))
                return index;
            index++;
        }
    }
    return -1;
}

​ 可以看到,如果给定的对象确实位于链表中,那么indexOf将可以返回一个大于-1的值;否则将返回-1, 而contains(Object o)则是做了一层封装,判断调用了indexOf之后得到的结果是否等于-1来返回相应的true或 者false;

3、一些关于队列特性的操作

1)常规的查看头节点、获取头节点、移除节点等
  • **peek();**查看头节点,但是不删除,这里返回的是头节点中保存的item对象,而不是返回整个节点元素;
  • **element();**同样获取头节点保存的元素的引用。同样不会删除节点;
  • **poll();**移除头节点,并且返回原头节点保存的item引用;实现其实是通过unlinkFirst(Node<E> f)完成相关工作;(点击查看源码分析)
  • **remove();**移除头节点;同样是返回被移除节点中保存的item对象
  • **offer(E e);**添加一个新元素,其实就是往链表中添加一个新的节点,所以调用了add(E e)方法;返回一个布尔值表示本次添加是否成功;(点击查看源码分析)
  • **offerFirst(E e);**添加一个新元素,但是使用头插法;(点击查看源码分析)

4、关于双向队列的操作:

  • 基本上与队列操作无异,只是因为每个链表中的每个节点都是有前驱和后继的,所以可以利用offerFirst(E e) offerLast(E e) peekFirst() peekLast() 分别可以查看存放在头节点或者尾节点的对象;pollFirst() pollLast()可以移除头尾节点;代码上基本上与队列那几个操作调用的实现一样,因此不再设置导航跳转;
  • 入队操作push(E e);头插法入队;
  • 出队操作pop(E e);移除头节点;
  • 两个对称操作:移除第一次/最后一次出现的对象;
    • removeFirstOccurrence(Object o);调用了remove(Object o)方法完成相关操作;返回一个boolean值表示移除是否成功;
    • removeLastOccurrence(Object o);实现代码与remove(Object o)的代码实现无异,核心区别在于此处将从前往后遍历改为从后往前遍历;同样返回一个boolean值表示删除是否成功;

5、链表迭代器

  • 既然没有实现RandomAccess接口,那就是推荐使用迭代器或者增强for循环来做遍历操作了,这样的话就需要引出LinkedList内部的私有类ListItr了;
1)成员属性
private Node<E> lastReturned;    //上次返回对象的存储节点
private Node<E> next;           //当前遍历的节点
private int nextIndex;          //下一次遍历到的索引
private int expectedModCount = modCount;    //期望的修改时间,如果对不上抛出抛出并发修改异常
2)构造器
ListItr(int index) {                //返回指定索引位置的节点引用
    // assert isPositionIndex(index);
    next = (index == size) ? null : node(index);
    nextIndex = index;
}
3)前/后遍历方法
public boolean hasNext() {      //索引是否越界
    return nextIndex < size;
}

//从前往后遍历
public E next() {              
    checkForComodification();    //这里是通过当前链表版本与迭代器所在版本是否一致,如果不一致将抛出并发修改异常
    if (!hasNext())
        throw new NoSuchElementException();

    lastReturned = next;     //保存当前节点的引用
    next = next.next;       //next指向下一个有效的引用
    nextIndex++;           //索引+1
    return lastReturned.item;  //返回当前的节点存放的对象
}

public boolean hasPrevious() {    //判断是否还有前驱
    return nextIndex > 0;
}

//从后往前遍历,与从前往后遍历大同小异
public E previous() {
    checkForComodification();
    if (!hasPrevious())
        throw new NoSuchElementException();

    lastReturned = next = (next == null) ? last : next.prev;
    nextIndex--;
    return lastReturned.item;
}

public int nextIndex() {
    return nextIndex;
}

public int previousIndex() {
    return nextIndex - 1;
}
4)涉及到修改的方法
//移除当前遍历到的节点
public void remove() {
    checkForComodification();     //检查迭代器的版本与链表的版本是否一致
    if (lastReturned == null)
        throw new IllegalStateException();

    Node<E> lastNext = lastReturned.next;
    unlink(lastReturned);   //通过是调用一个通用的私有移除方法,可以点击标题跳转相应的位置查看分析
    if (next == lastReturned)
        next = lastNext;
    else
        nextIndex--;
    lastReturned = null;
    expectedModCount++;
}
//修改当前节点的值为e
public void set(E e) {
    if (lastReturned == null)
        throw new IllegalStateException();
    checkForComodification();
    lastReturned.item = e;
}
//添加一个新的结点在尾部
public void add(E e) {
    checkForComodification();
    lastReturned = null;
    if (next == null)
        linkLast(e);
    else
        linkBefore(e, next);
    nextIndex++;
    expectedModCount++;
}

//遍历,并且执行action指定的操作
//Consumer是一个函数式接口,可以利用lambda表达式来操作链表的节点
public void forEachRemaining(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    while (modCount == expectedModCount && nextIndex < size) {
        action.accept(next.item);    //调用指定的操作
        lastReturned = next;
        next = next.next;
        nextIndex++;
    }
    checkForComodification();
}

//保证并发下的遍历不会产生任何未知的隐患,因为在并发修改情况下,该迭代器的会迅速而干净地失败,并且不允许被重写
final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}
  • 除了一个灵活的迭代器之外,LinkedList也提供了另一个降序的迭代器,该迭代器只能从尾节点开始往前遍历,但是具体实现是ListItr,所以在并发修改的情况下也是会迅速而干净地失败;
private class DescendingIterator implements Iterator<E> {
    private final ListItr itr = new ListItr(size());
    public boolean hasNext() {
        return itr.hasPrevious();
    }
    public E next() {
        return itr.previous();
    }
    public void remove() {
        itr.remove();
    }
}

6、剩余的一些方法

  • 接下来的一下方法都是做一些简单的分析,不再结合源码一同分析,因为个人感觉剩下的方法,在一般使用的情况下并不常用,所以节省笔墨;
  • **clone()**此复制为浅复制,而且节点中的对象并不会被复制,所以一旦利用该方法克隆到的新链表发生修改,也会影响旧的;
  • toArray() / toArray(T[] a); 前者返回的是一个对象数组,在调用者获取到之后仍然需要进行类型转换,而后者则是通过泛型约束,可以获取到确切的对象数组,如果给定的数组容量不足以装载所有节点存储的对象,则会自动通过反射来创建一个相应的对象数组,容量扩展为链表的长度;
  • writeObject(java.io.ObjectOutputStream s) / readObjec(java.io.ObjectInputStream s),因为实现了Serializable接口,所以这两个方法负责序列化和反序列化链表;

7、总结

  • 经过了一段略微有点漫长的阅读之后,终于是完成对于LinkedList的代码解读,那么接下里就是会做一些轻松的总结;
  • 因为LinkedList的数据结构为链表,所以,理论上链表的容量取决于虚拟机的可用内存大小;这是不同于ArrayList有一个明显的内存上限的;
  • LinkedList的组成节点都是双向的,有一个前驱与一个后继,所以可以对链表实现比较自由的遍历操作;当然这种自由是相对而言的,而且推荐使用增强循环来遍历或者利用迭代器,这样效率会比较高;
  • LinkedList不是线程安全的,同时在并发情况下利用其迭代器进行遍历 也比较容易失败,因为一旦发生修改,迭代器就会失效,原因上文分析的时候也有叙述;
  • LinkedList适用于插入和删除比较频繁的情况,时间复杂度为O(x),x取决于节点所在的位置,即index;在插入索引有效的前提下,最大遍历次数不超过链表当前长度的一半;
  • 虽然LinkedList也支持随机访问,但是这是建立在需要遍历寻找的前提下,所以效率不高;
个人理解:
  • 如果真要把这些几毫秒的微妙效率放大来看的话,虽然说LinkedList在插入删除方面的性能高,但是如果是在中间插入的话,还是要经历一波遍历查找,这里的性能不太好说;
  • 其次,如果是在ArrayList的边缘进行插入,而且不触发扩容的话,其实算上位置调整这些操作也不会有太慢的效率;
  • 总的来说,在高级语言发展到了现在的程度,单单去考虑这一点点效率而言,其中带来的意义好像不太大,因为语言本身的优化也足以弥补上一些效率上的问题;
  • 与其过于看重那一点点效率的提升,还不如视情况选择合适的数据结构,这样带来的优化就不言而喻了。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值