JDK源码解析:LinkedList

1、背景

我们咨询一下腾讯混元大模型,什么是"LinkedList"。

以下是混元大模型的回答:

nginx 复制代码
LinkedList 是 Java 集合框架中的一种数据结构,它实现了 List 和 Deque 接口。LinkedList 是一个双向链表,这意味着每个元素都包含对前一个和后一个元素的引用。
这种数据结构允许在列表的两端进行高效的插入和删除操作。

LinkedList 的主要特点:

  1. 动态大小:与数组不同,LinkedList 的大小可以动态增长或缩小。

  2. 高效的插入和删除:在 LinkedList 的开头、结尾或中间插入或删除元素时,不需要移动其他元素,因此这些操作通常比在 ArrayList 中执行得更快。

  3. 内存效率:由于不需要连续的内存空间来存储元素,LinkedList 在内存使用上可能比 ArrayList 更有效。

  4. 访问速度:与 ArrayList 相比,LinkedList 的随机访问速度较慢,因为需要从头部或尾部开始遍历链表才能找到特定位置的元素。

  5. 线程不安全:LinkedList 不是线程安全的。如果多个线程同时修改 LinkedList,可能会导致数据不一致。在多线程环境中,可以使用 Collections.synchronizedList() 方法将 LinkedList 包装成线程安全的列表,或者使用 CopyOnWriteArrayList(尽管它更适合读多写少的场景)。

2、源码分析

2.1类定义

cpp 复制代码
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements     List<E>, Deque<E>, Cloneable, java.io.Serializable
{}

源码分析:

  • LinkedList 继承了AbstractSequentialList

    • AbstractSequentialList 是 Java 集合框架中的一个抽象类,它实现了 List 接口并提供了一个顺序访问列表元素的迭代器。AbstractSequentialList 为那些希望以顺序方式访问其元素的列表提供了一个通用的基础实现。
  • LinkedList 实现了List接口:具备线性表的操作,如:

    • size、isEmpty、contains、containsAll

    • add、addAll、removeAll、retainAll、clear、subList

  • LinkedList 实现了Deque接口:具备双向链表的操作,如:

    • addFirst、removeFirst、pollFirst、getFirst、peekFirst

    • addLast、removeLast、pollLast、getLast、peekLast

2.2基本属性

java 复制代码
transient int size = 0;


/**
 * Pointer to first node.
 */
transient Node<E> first;


/**
 * Pointer to last node.
 */
transient Node<E> last;


/** 修改次数 */
protected transient int modCount = 0;

源码分析:

  • LinkedList 内置了两个指针,包括头结点first和末尾指针last

  • LinkedList 也设置了size,标识有效元素数量(不包括头结点和末尾指针)

  • LinkedList设置了modCount,标识修改操作次数,modCount字段用于跟踪列表的结构修改次数,以确保在迭代过程中发生并发修改时能够快速失败,会直接触发异常ConcurrentModificationException

2.3 基本操作:增删改查

(1)增加元素

通过阅读源码,LinkedList有7种添加元素方法,

  1. add(E e):在列表的末尾添加一个元素(默认在列表的末尾添加,即尾插法)

  2. add(int index, E element):在指定位置插入一个元素。

  3. addFirst(E e):在列表的开头添加一个元素。

  4. addLast(E e):在列表的末尾添加一个元素(与 add(E e) 相同)。

  5. push(E e):在列表的开头添加一个元素(与 addFirst(E e) 相同)。

  6. addAll的两个重载方法:则是批量插入元素

解析 add(E e) 方法源码
java 复制代码
public boolean add(E e) {
    linkLast(e);
    return true;
}


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++;
}


Node(Node<E> prev, E element, Node<E> next) {
    this.item = element;
    this.next = next;
    this.prev = prev;
}

源码分析:

  • linkLast(e):尾插法

  • 新建Node节点:前继指针指向last,当前数据为e,后继指针为null

  • 容量size+1,修改次数+1

(2)删除元素

解析remove()方法源码

源码分析:

java 复制代码
public E remove() {
    return removeFirst();
}


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


private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    // 【1】
    final Node<E> next = f.next;
    // 【2】
    f.item = null;
    // 【3】
    f.next = null; // help GC
    // 【4】
    first = next;
    // 【5】
    if (next == null)
        last = null;
    else
        // 【6】
        next.prev = null;
    // 【7】
    size--;
    modCount++;
    // 【8】
    return element;
}

源码解析:

  1. 中间变量:next局部变量 承载被删节点的next指针(下个元素地址)

  2. 设置被删节点的item数值为null

  3. 设置被删节点的next指针为null

  4. 设置first指针为next局部变量 (下个元素地址)

  5. 如果next局部变量为null,说明没有元素了,顺带设置last为null

  6. 否则设置next局部变量 的前继指针为null,因为此后next局部变量 的元素为头结点了

  7. 容量-1,操作次数+1

  8. 返回被删数据

(3)修改元素

更新set()方法源码
java 复制代码
public E set(int index, E element) {
    // 【1】
    checkElementIndex(index);
    // 【2】
    Node<E> x = node(index);
    // 【3】
    E oldVal = x.item;
    // 【4】
    x.item = element;
    // 【5】
    return oldVal;
}


Node<E> node(int index) {
    // assert isElementIndex(index);
    // 【2.1】右移位运算,size/2
    if (index < (size >> 1)) {
        // 【2.2】
        Node<E> x = first;
        // 【2.3】从头部进行遍历
        for (int i = 0; i < index; i++)
            // 【2.4】
            x = x.next;
        // 【2.5】
        return x;
    } else {
        // 【2.6】从尾部进行遍历
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            // 【2.7】
            x = x.prev;
        // 【2.8】
        return x;
    }
}


private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}


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

源码分析:

  1. 检查元素是否合法,不合法抛出异常IndexOutOfBoundsException

  2. 通过遍历链表,得到元素地址

    1. 计算要更新的索引下标,离first更近,还是离last更近

    2. 离first更近,从头部开始遍历,for循环遍历,得到前一个元素的next指向的元素地址

    3. 离last更近,从尾部开始遍历,for循环遍历,得到后一个元素的prev指向的元素地址

  3. 获取更新前数值

  4. 更新新数值

  5. 返回更新前数值

(4)获取元素

获取某个索引下标get()方法源码
cs 复制代码
public E get(int index) {
    // 【1】
    checkElementIndex(index);
    // 【2】
    return node(index).item;
}


private void checkElementIndex(int index) {
    if (!isElementIndex(index))
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}


Node<E> node(int index) {
    // assert isElementIndex(index);
    // 【2.1】右移位运算,size/2
    if (index < (size >> 1)) {
        // 【2.2】
        Node<E> x = first;
        // 【2.3】从头部进行遍历
        for (int i = 0; i < index; i++)
            // 【2.4】
            x = x.next;
        // 【2.5】
        return x;
    } else {
        // 【2.6】从尾部进行遍历
        Node<E> x = last;
        for (int i = size - 1; i > index; i--)
            // 【2.7】
            x = x.prev;
        // 【2.8】
        return x;
    }
}

源码解析:

(会发现,其实获取元素的逻辑,就是修改元素的前置操作)

  1. 检查元素是否合法,不合法抛出异常IndexOutOfBoundsException

  2. 通过遍历链表,得到元素地址

    1. 计算要更新的索引下标,离first更近,还是离last更近

    2. 离first更近,从头部开始遍历,for循环遍历,得到前一个元素的next指向的元素地址

    3. 离last更近,从尾部开始遍历,for循环遍历,得到后一个元素的prev指向的元素地址

3、总结

LinkedList 是一个双向链表,这意味着每个元素都包含对前一个和后一个元素的引用。这种数据结构允许在列表的两端进行高效的插入和删除操作。

3.1 ArrayList和LinkedList比较

  1. ArrayList底层基于动态数组实现,LinkedList底层基于链表实现

  2. 对于随机访问(get/set方法),ArrayList通过index直接定位到数组对应位置的节点,而LinkedList需要从头结点或尾节点开始遍历,直到寻找到目标节点,因此在效率上ArrayList优于LinkedList

  3. 对于插入和删除(add/remove方法),ArrayList需要移动目标节点后面的节点(使用System.arraycopy方法移动节点),而LinkedList只需修改目标节点前后节点的next或prev属性即可,因此在效率上LinkedList优于ArrayList。

相关推荐
清风fu杨柳7 分钟前
centos7 arm版本编译qt5.6.3详细说明
开发语言·arm开发·qt
醉颜凉10 分钟前
【NOIP提高组】潜伏者
java·c语言·开发语言·c++·算法
_小柏_12 分钟前
C/C++基础知识复习(20)
开发语言
阿维的博客日记14 分钟前
java八股-jvm入门-程序计数器,堆,元空间,虚拟机栈,本地方法栈,类加载器,双亲委派,类加载执行过程
java·jvm
qiyi.sky15 分钟前
JavaWeb——Web入门(8/9)- Tomcat:基本使用(下载与安装、目录结构介绍、启动与关闭、可能出现的问题及解决方案、总结)
java·前端·笔记·学习·tomcat
lapiii35819 分钟前
图论-代码随想录刷题记录[JAVA]
java·数据结构·算法·图论
RainbowSea21 分钟前
4. Spring Cloud Ribbon 实现“负载均衡”的详细配置说明
java·spring·spring cloud
程序员小明z22 分钟前
基于Java的药店管理系统
java·开发语言·spring boot·毕业设计·毕设
爱敲代码的小冰41 分钟前
spring boot 请求
java·spring boot·后端
Lyqfor1 小时前
云原生学习
java·分布式·学习·阿里云·云原生