Java集合之LinkedList源码解析

31 篇文章 0 订阅
16 篇文章 2 订阅

目录

1.LinkedList

1.1整体架构

1.2追加源码分析

1.3节点删除

1.4节点查询

2.迭代器ListIterator

2.1整体架构

2.2三个方法源码解析


1.LinkedList


1.1整体架构

  • 底层使用了双向链表来进行实现,有一个头节点,尾节点和一个元素个数size
  • 这里的头节点和尾节点不是哨兵的概念,只是单纯的指针指向概念!!!
public class LinkedList<E>
{
    //元素个数
    transient int size = 0;//transient 表示不可序列化字段
    //头节点
    transient Node<E> first;
    //尾节点
    transient Node<E> last;
}
  • 每个Node有prev和next指针以及val节点值
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;
    }
}

1.2追加源码分析

  • 尾部追加linkLast
// 从尾部开始追加节点
void linkLast(E e) {
    // 把尾节点数据暂存
    final Node<E> l = last;
    // 新建新的节点,初始化入参含义:
    // l 是新节点的前一个节点,当前值是尾节点值
    // e 表示当前新增节点,并且当前新增节点后一个节点是 null
    final Node<E> newNode = new Node<>(l, e, null);
    // 更新尾节点指针指向
    last = newNode;
    // 判断链表是否为空
    if (l == null) // 通过头节点也可以进行判断
        // 更新头节点指针指向
        first = newNode;
    // 否则,把前尾节点的下一个节点,指向当前尾节点。
    else
        l.next = newNode;
    // 更新size大小和版本号
    size++;
    modCount++;
}

首先预存尾节点的指针作为旧的尾节点,在新建一个节点,并且设置该节点的prev和next指针

在更新尾节点,在判断链表是否为空,空链表需要更新头节点,不为空就需要更新旧的尾节点的next指针

最后需要更新size的大小和版本号的值

  • 头部追加linkFirst
// 从头部追加
private void linkFirst(E e) {
    // 头节点赋值给临时变量
    final Node<E> f = first;
    // 新建节点,前一个节点指向null,e 是新建节点,f 是新建节点的下一个节点,目前值是头节点的值
    final Node<E> newNode = new Node<>(null, e, f);
    // 更新头节点的指针指向
    first = newNode;
    // 判断链表是否为空
    if (f == null)
        // 更新尾节点的指针指向
        last = newNode;
    // 旧的头节点的前继指针指向新的头节点
    else
        f.prev = newNode;
    // 更新链表长度和版本号
    size++;
    modCount++;
}

头部追加和尾部追加,基本是相同的方法。

预存头节点的指针,新建节点,更新头节点的指针指向,判断链表是否为空(为空需要更新尾节点指针指向,不为空需要更新旧的头节点的前驱指针),最后更新链表的长度和版本号

1.3节点删除

跟追加类似,也可以头部删除和尾部删除,这里只分析头部删除

//从头删除节点 断言:f 是链表头节点
//一般配合removeFirst方法来使用
private E unlinkFirst(Node<E> f) {
    // 拿出头节点的值,作为方法的返回值
    final E element = f.item;
    // 拿出头节点的下一个节点
    final Node<E> next = f.next;
    //帮助 GC 回收头节点
    f.item = null;
    f.next = null;
    // 更新头节点的指针指向为next节点
    first = next;
    //如果 next 为空,表明链表为空
    if (next == null)
        last = null;
    //链表不为空,头节点的前一个节点指向 null
    else
        next.prev = null;
    //修改链表大小和版本
    size--;
    modCount++;
    return element;
}

public E removeFirst() {
    final Node<E> f = first;
    //断言头节点
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}

unlinkFirst方法一般配合removeFirst方法来进行使用

先获取要删除节点的值用于返回,在获取到头节点的next节点,在设置头节点的值和next指针为null帮助GC回收(前驱节点一定为null),

在更新头节点的指针指向为next,在判断删除后链表是否为空(为空需要更新尾节点,不为空需要更新next节点的前驱指针为null),最后更新size大小和版本号;

1.4节点查询

// 根据链表索引位置查询节点
// 一般配合get方法进行使用
Node<E> node(int index) {
    // 如果 index 处于队列的前半部分,从头开始找
    if (index < (size >> 1)) {
        Node<E> x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {// 如果 index 处于队列的后半部分,从尾开始找
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

public E get(int index) {
        // 检验index是否安全
        checkElementIndex(index);
        return node(index).item;
    }

node方法一般是配合get方法来进行使用的

利用了双向链表和属性封装的特点,耗时可以减少一半

如果index索引位置在前半区,就从头开始找,如果index索引位置在后半区,就从尾部开始找

ps:注意是索引位置,从0开始


 

2.迭代器ListIterator


2.1整体架构

  • 使用Iterator只能实现单向遍历和三个方法,比较单一,在Iterator类的基础上增加了双向遍历
// 双向迭代器
private class ListItr implements ListIterator<E> {
    private Node<E> lastReturned;// 当前指向的节点
    private Node<E> next;// 下一个节点
    private int nextIndex;// 下一个节点的位置
    private int expectedModCount = modCount;// 期望版本号
    ListItr(int index) {
        // 会初始化next节点
        // 比如index=7,会找到第八个元素,但是只会取到前七个元素,需要注意
        next = (index == size) ? null : node(index); //根据索引位置查找
        nextIndex = index;
    }
}

ps:要注意新维护了一个nextIndex来指代next的意义

ps:比如index8,会找到第八个元素,但是只会取到前七个元素,需要注意

2.2三个方法源码解析

// 在这里nextIndex代表的是节点的个数
public boolean hasPrevious() {
    return nextIndex > 0;
}
// 取前一个节点
public E previous() {
    checkForComodification();
    if (!hasPrevious())
        throw new NoSuchElementException();
    // next 为空场景:1:说明是第一次迭代,取尾节点(last);2:上一次操作把尾节点删除掉了
    // next 不为空场景:说明已经发生过迭代了,直接取前一个节点即可(next.prev)
    lastReturned = next = (next == null) ? last : next.prev; //在这里next和lastReturned一直一样
    // 更新索引位置
    nextIndex--;
    return lastReturned.item;
}

首先检查版本号,在判断是否有前继节点可以迭代,在判断是否是第一次迭代(是第一次迭代那么next节点为null,不是第一次向前遍历即可),

然后更新索引的位置,最后返回当前指针节点的元素

ps:nextIndex在这里代表的是节点的数量,

ps:next和lastReturned的值会一直一样,在这里下一个节点和当前节点指向同一个位置

// 在这里nextIndex代表的是索引位置
public boolean hasNext() {
    return nextIndex < size;// 下一个节点的索引小于链表的大小,就有
}

// 取下一个元素
public E next() {
    //检查期望版本号有无发生变化
    checkForComodification();
    //进行检查
    if (!hasNext())
        throw new NoSuchElementException();
    // 第一次执行时,是在初始化迭代器的时候,next 是被赋值的
    lastReturned = next; //返回当前指针位置的元素,next和lastReturned始终差一个
    // 更新next节点,为下次迭代做准备
    next = next.next;
    // 更新索引位置
    nextIndex++;
    return lastReturned.item;
}

方法流程大致与previous相同:检验版本号,在判断是否可以迭代,在更新当前元素的指针和下一个元素的指针

ps:在这里nextIndex代表的值为下一个节点的索引位置,默认为0

ps:next和lastReturned每次差一个元素,next指代下一个元素,lastReturned指代当前元素

方法小结:

  • 初始化方法应传入的是节点数量,而不是索引值的大小,比如7,对于previous方法会索引到前7个元素,next方法会索引到剩余size-7个元素;
  • previous方法的当前节点元素和前一个节点元素相同,next的当前节点元素和前一个节点位置总是差一;
  • previus方法停止的判断是nextIndex==0的时候,next方法停止的判断是nextIndext==size的时候;
public void remove() {
    checkForComodification();
    // lastReturned 为空,说明调用者没有主动执行过 next() , previos() 或者 重复删除
    // lastReturned 不为空,是在上次执行 next() 或者 previos()方法时赋的值
    if (lastReturned == null)
        throw new IllegalStateException();
    //反向遍历的时候需要用到
    Node<E> lastNext = lastReturned.next;
    //删除当前节点
    unlink(lastReturned);
    // 从尾到头遍历,next == lastReturned始终会成立,next不能为null的因为还需要遍历
    if (next == lastReturned)
        next = lastNext;
    else //从头到尾遍历
        nextIndex--;
    //防止重复删除,当前指针元素置为null
    lastReturned = null;
    //更新版本号
    expectedModCount++;
}

先进行版本号检查,在判断是否重复删除当前元素,在获取当前元素的next值(反向遍历时,要给next赋值否则无法继续遍历),在进行删除当前节点,

在判断是否是反向遍历(是反向遍历就要给next赋值防止无法遍历,nextIndex表示的是元素的数量,因为当前元素是已经遍历过的,所以不需要在减,是正向遍历next值不影响,nextIndex表示的是元素的索引位置需要减一)

在就是为了防止重复删除,并且标识当前元素已经删除会设置为null,最后更新版本号

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值