Java集合源码剖析——基于JDK1.8中LinkedList的实现原理(上)

简介: Java集合源码剖析——基于JDK1.8中LinkedList的实现原理 (上)

文章目录:

1.看看关于LinkedList源码开头的注释

2.LinkedList中的属性

3.LinkedList中的方法

3.1 pushoffer方法

3.2 添加元素的一系列add方法

3.3 linkFirst方法

3.4 linkLast方法

3.5 linkBefore方法

3.6 移除元素的一系列remove方法


1.看看关于LinkedList源码开头的注释


* Doubly-linked list implementation of the {@code List} and {@code Deque}

* interfaces.  Implements all optional list operations, and permits all

* elements (including {@code null}).

*

* <p>All of the operations perform as could be expected for a doubly-linked

* list.  Operations that index into the list will traverse the list from

* the beginning or the end, whichever is closer to the specified index.

*

* <p><strong>Note that this implementation is not synchronized.</strong>

* If multiple threads access a linked list concurrently, and at least

* one of the threads modifies the list structurally, it <i>must</i> be

* synchronized externally.  (A structural modification is any operation

* that adds or deletes one or more elements; merely setting the value of

* an element is not a structural modification.)  This is typically

* accomplished by synchronizing on some object that naturally

* encapsulates the list.


从这段注释中,我们可以得知 LinkedList 是通过一个双向链表来实现的,它允许插入所有元素,包括 null,同时,它是线程不同步的。


·       LinkedList集合底层结构是带头尾指针的双向链表。

·       LinkedList是非线程安全的。

·       LinkedList集合中存储元素的特点:有序可重复,元素带有下标,从0开始,以1递增。

·       LinkedList集合的优点:在指定位置插入/删除元素的效率较高;缺点:查找元素的效率不如ArrayList

如果对双向链表这个数据结构很熟悉的话,学习 LinkedList 就没什么难度了。下面是双向链表的结构:


双向链表每个结点除了数据域之外,还有一个前指针和后指针,分别指向前驱结点和后继结点(如果有前驱/后继的话)。另外,双向链表还有一个 first 指针,指向头节点;和 last 指针,指向尾节点。

2.LinkedList中的属性


//双向链表中结点个数
transient int size = 0;
//指向头结点的指针
transient Node<E> first;
//指向尾结点的指针
transient Node<E> last;

关于LinkedList中的Node节点结构,它其实是在 LinkedList 里定义的一个静态内部类,它表示链表每个节点的结构,包括一个数据域 item,一个后置指针 next,一个前置指针 prev

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

3.LinkedList中的方法


3.1 push、offer方法

pushoffer方法内部都是调用的相应的add方法,所以直接看下面的add方法源码解析。

public void push(E e) {
    addFirst(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;
}

3.2 添加元素的一系列add方法

LinkedList集合源码中,添加元素很多时候都会用到 addaddFirstaddLast 这几个,而查看源码发现,这几个方法的内部实际上都调用了 linkFirstlinkLast linkBefore 这三个方法。

public boolean add(E e) {
    linkLast(e);
    return true;
}
public void addFirst(E e) {
    linkFirst(e);
}
public void addLast(E e) {
    linkLast(e);
}

所以下面着重来说一下 linkFirstlinkLast linkBefore 这三个方法。


3.3 linkFirst方法

对于链表这种数据结构来说,添加元素的操作无非就是在表头/表尾插入元素,又或者是在指定位置插入元素。因为 LinkedList 有头指针和尾指针,所以在表头或表尾进行插入元素只需要 O(1) 的时间,而在指定位置插入元素则需要先遍历一下链表,所以复杂度为 O(n)


linkFirst表面上翻译就是链表首位,也就是在表头添加元素,其源码及添加元素分析过程如下:

private void linkFirst(E e) {
    final Node<E> f = first;
    final Node<E> newNode = new Node<>(null, e, f);
    first = newNode;
    if (f == null)
        last = newNode;
    else
        f.prev = newNode;
    size++;
    modCount++;
}

当我们向双向链表的表头插入一个结点 e 时,这个结点的前驱指针肯定是 null,那么在插入之后,这个 e 结点的后继指针是什么呢?(在e插入之前,原先的表头结点就是表头结点,在e插入到原表头的前面成为了新的表头之后,此时原表头结点不就是当前e结点的后继结点了吗?所以e结点的后继指针自然也就指向了原表头结点,所以这里e的后继指针就是最初链表的头指针first),所以要在插入之前先获取到头指针 f = first,然后将头指针对应的结点修改为新插入的结点enewNode),对应源码的前三行。


而这个if判断的是:如果在插入之前链表的头指针为空(换句话说,当前链表中没有元素,e结点插入之后只有这一个元素),那么此时e结点肯定既是头结点、也是尾结点啊,所以这里将 last 尾结点修改为刚刚插入的 e 结点。

下面的else是说:当前链表中有多个元素了话,当你在某个结点x之前新插入一个结点e,那么结点x的后继部分肯定没有变化,变化的是它的前驱部分,前驱指针所指内容就变成了新插入的结点e,即 f.prev = newNode


3.4 linkLast方法

这个方法和 linkFirst 差不多的,无非是一个在表头插入元素,一个在表尾插入元素。

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

首先插入之前,先获取一下原表尾结点的内容(l = last),然后插入新的结点e,这个结点e的后继指针肯定是 null(因为此时它成了链表的表尾结点),而它的前驱指针指向的就应该是上一个表尾结点 l,所以新表尾结点e的内容就是(lenull)。然后更新双向链表的尾指针 last 为新插入的结点newNode


下面的if判断的是:如果在插入之前获取的尾指针 l 为空(也就是说当前链表中没有元素),那么新插入一个元素之后,这个元素肯定既是头结点、也是尾结点,所以这里将它的头指针也更新为 newNode


else说的是:当插入之前链表中有多个元素,那么在表尾新插入一个元素之后,还要最终修改一下原表尾结点的后继指针,让它指向新的表尾结点。


3.5 linkBefore方法

上面两个方法分别都是在表头、表尾插入元素,那么现在该说一下在中间某个随机位置插入元素的方法了,源码如下:

void linkBefore(E e, Node<E> succ) {
    // assert succ != null;
    final Node<E> pred = succ.prev;
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

对应源码来说,这里要插入的结点就是 ee要插入到 succ 结点的前面,同时位于 pred 结点的后面,而在插入之前(succ的前驱就是pred),所以先获取到这个前驱结点 pred,然后修改e结点的内容(predesucc),那么此时 succ 的前驱就变成了 e,所以更新 succ 的前驱指针指向 newNode


if判断的是:如果插入之前succ结点的前驱指针pred为空,也就是说此时succ是表头结点,那么e插入之后,它就成了新的表头结点,所以这里让first头指针指向newNode


else则是说插入之前链表中有多个元素,那么在succ指定结点的前面插入新的结点之后,还要修改succ原前驱节点pred的后继指针,使其指向刚插入的e结点newNode


3.6 移除元素的一系列remove方法

LinkedList集合源码中,移除元素很多时候都会用到 removeremoveFirstremoveLast 这几个,而查看源码发现,这几个方法的内部实际上都调用了 unlinkFirstunlinkLast unlink 这三个方法。unlink方法中的node方法是对链表进行遍历的。

public E remove() {
    return removeFirst();
}
public E remove(int index) {
    checkElementIndex(index);
    return unlink(node(index));
}
public boolean remove(Object o) {
    if (o == null) {
        for (Node<E> x = first; x != null; x = x.next) {
            if (x.item == null) {
                unlink(x);
                return true;
            }
        }
    } else {
        for (Node<E> x = first; x != null; x = x.next) {
            if (o.equals(x.item)) {
                unlink(x);
                return true;
            }
        }
    }
    return false;
}
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);
}

所以下面着重来说一下 unlinkFirstunlinkLast unlink 这三个方法。

相关文章
|
14天前
|
存储 缓存 安全
Java 集合江湖:底层数据结构的大揭秘!
小米是一位热爱技术分享的程序员,本文详细解析了Java面试中常见的List、Set、Map的区别。不仅介绍了它们的基本特性和实现类,还深入探讨了各自的使用场景和面试技巧,帮助读者更好地理解和应对相关问题。
35 5
|
26天前
|
存储 缓存 安全
Java 集合框架优化:从基础到高级应用
《Java集合框架优化:从基础到高级应用》深入解析Java集合框架的核心原理与优化技巧,涵盖列表、集合、映射等常用数据结构,结合实际案例,指导开发者高效使用和优化Java集合。
38 4
|
1月前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
33 2
|
1月前
|
安全 Java
Java多线程集合类
本文介绍了Java中线程安全的问题及解决方案。通过示例代码展示了使用`CopyOnWriteArrayList`、`CopyOnWriteArraySet`和`ConcurrentHashMap`来解决多线程环境下集合操作的线程安全问题。这些类通过不同的机制确保了线程安全,提高了并发性能。
|
3月前
|
Java
安装JDK18没有JRE环境的解决办法
安装JDK18没有JRE环境的解决办法
382 3
|
3天前
|
NoSQL 关系型数据库 MySQL
Linux安装jdk、mysql、redis
Linux安装jdk、mysql、redis
60 7
|
4月前
|
Oracle Java 关系型数据库
Mac安装JDK1.8
Mac安装JDK1.8
784 4
|
4月前
|
Java 关系型数据库 MySQL
"解锁Java Web传奇之旅:从JDK1.8到Tomcat,再到MariaDB,一场跨越数据库的冒险安装盛宴,挑战你的技术极限!"
【8月更文挑战第19天】在Linux上搭建Java Web应用环境,需安装JDK 1.8、Tomcat及MariaDB。本指南详述了使用apt-get安装OpenJDK 1.8的方法,并验证其版本。接着下载与解压Tomcat至`/usr/local/`目录,并启动服务。最后,通过apt-get安装MariaDB,设置基本安全配置。完成这些步骤后,即可验证各组件的状态,为部署Java Web应用打下基础。
66 1
|
1月前
|
Oracle Java 关系型数据库
安装 JDK 时应该注意哪些问题
选择合适的JDK版本需考虑项目需求与兼容性,推荐使用LTS版本如JDK 17或21。安装时注意操作系统适配,配置环境变量PATH和JAVA_HOME,确保合法使用许可证,并进行安装后测试以验证JDK功能正常。
55 1
|
1月前
|
IDE Java 编译器
开发 Java 程序一定要安装 JDK 吗
开发Java程序通常需要安装JDK(Java Development Kit),因为它包含了编译、运行和调试Java程序所需的各种工具和环境。不过,某些集成开发环境(IDE)可能内置了JDK,或可使用在线Java编辑器,无需单独安装。
72 1