摘要:java.util.LinkedList 是 Java 集合框架中的成员之一,底层是基于双向链表实现,集合容量可动态变化的。

本文分享自华为云社区《LinkedList 源码分析》,作者: 陈皮的JavaLib。

LinkedList 简介

java.util.Linked List 是 Java 集合框架中的成员之一,底层是基于双向链表实现,集合容量可动态变化的。它继承自 Abstract Sequential List 抽象类,实现了 List 接口。同时还实现了 Cloneable 和 Serializable 三个标记接口,说明 Array List 是可克隆复制的,可序列化的。

Array List 数组列表底层是基于动态数组实现的,所以优点是能支持快速随机访问,但是增删操作可能会比较慢(因为可能需要进行数组扩容,数据拷贝)。而且数组需要先申请一定的内存空间,可能会造成浪费。而链表列表 LinkedList 的优点是增删操作速度比较快,而且列表存储多少元素就动态申请多少节点来存储,比较节省内存空间。

为何要使用双向链表呢,主要在于遍历效率比单向链表高。例如当我们需要查找指定下标的节点,在指定下标进行增删改操作时,先判断这个位置是靠近头部还是尾部,从而决定从头部还是从尾部开始查找,提高效率。

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

2 LinkedList 源码分析

2.1 内部变量

LinkedList 的元素是存储在节点对象中的,节点类是 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;
}
}

LinkedList 中定义了3个变量,一个代表当前列表的元素个数,另外两个变量指向链表的头部和尾部。以及它的父类 AbstractList 中的 modCount 变量,每次对链表的增删改操作都会使它加1。

transient int size = 0;

transient Node<E> first;

transient Node<E> last;

protected transient int modCount = 0;

2.2 构造函数

ArrayList 有2个构造函数,一个无参构造函数,另一个使用指定 Collection 集合来构造集合的构造函数。

无参构造函数,什么都没有操作。

public LinkedList() {}

使用指定 Collection 集合来构造链表,如果 Collection 不能为 null ,否则会抛出 npe 。

public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
} 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)
return false; Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
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;
succ.prev = pred;
} size += numNew;
modCount++;
return true;
}

2.3 常用方法

  • public E getFirst()

获取链表的第一个元素,如果不存在第一个节点,抛出异常。

public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
  • public E getLast()

获取链表的最后一个元素,如果链表为空,则抛出异常。

public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
  • public E removeFirst()

删除第一个元素,如果链表为空,则抛出异常。

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

删除最后一个元素,如果链表为空,则抛出异常。

public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
  • public void clear()

情况链表,遍历每一个节点,将每一个节点的内部引用都置为 null ,便于进行垃圾回收。

public void clear() {
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}
  • public boolean add(E e)

在链表尾部添加一个元素。

public boolean add(E e) {
linkLast(e);
return true;
}
  • public Iterator iterator()

获取 list 的迭代器,用于遍历集合中的元素。

public Iterator<E> iterator() {
return new Itr();
}
  • public int size():返回集合元素个数。
  • public boolean contains(Object o):是否包含某个元素。
  • public boolean remove(Object o):删除某个元素。
  • public E get(int index):获取指定下标的元素。
  • public E set(int index, E element):在指定下标修改元素值。
  • public void add(int index, E element):在指定下标添加元素。

3 常见面试题分析

3.1 LinkedList 是线程安全的吗?

我们通过分析源码可知,对它的任何操作都是没有加锁的,所以在多线程场景下,它是线程不安全的。它适合在非多线程使用场景下,并且增删操作比较多的情况。

public static void main(String[] args) throws InterruptedException {

    LinkedList<String> list = new LinkedList<>();

    Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
list.add(Thread.currentThread().getName() + i);
}
}, "Thread01");
thread1.start(); Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
list.add(Thread.currentThread().getName() + i);
}
}, "Thread02");
thread2.start(); thread1.join();
thread2.join(); System.out.println(list.size()); // 输出不一定是2000,例如1850 }

如果增删操作比较多的话,可以使用 LinkedList ,LinkedList 增删操作速度比较快。

如果需要线程安全的话,可以使用 JDK 集合中的工具类 Collections 提供一个方法 synchronizedList 可以将线程不安全的 List 集合变成线程安全的集合对象,如下所示。

public static void main(String[] args) throws InterruptedException {

    LinkedList<String> list = new LinkedList<>();
// 封装成线程安全的集合
List<String> synchronizedList = Collections.synchronizedList(list); Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronizedList.add(Thread.currentThread().getName() + i);
}
}, "Thread01");
thread1.start(); Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
synchronizedList.add(Thread.currentThread().getName() + i);
}
}, "Thread02");
thread2.start(); thread1.join();
thread2.join(); System.out.println(synchronizedList.size()); }

3.2 LinkedList 优缺点

  • 优点:增删操作速度快,不仅有头部和尾部双指针,可以根据要操作的下标靠近哪边,从而决定从哪一边开始遍历找到指定的下标。找到位置后,删除和插入操作的时间复杂度为 O(1) 。
  • 缺点:不支持快速随机访问,相对 ArrayList 比较慢,但也不是决定的,取决于列表的长度,以及访问的下标位置。

3.3 使用迭代器 Iterator 过程中,可以增删元素吗?

通过源码分析,在获取集合的迭代器方法中,返回的是 AbstractList 抽象类中定义的 ListItr 迭代器对象,在他的父类 Itr 中持有变量 expectedModCount ,在初始化迭代器对象时这个变量的值被赋予此时链表中的操作次数 modCount 。在迭代获取元素时,会检查这两变量是否相等,不相等则抛出并发修改异常。所以不支持在使用迭代器的过程中,对原链表进行增删改操作。但是可以调用迭代器的增删操作。

private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
cursor = index;
} public boolean hasPrevious() {
return cursor != 0;
} public E previous() {
checkForComodification();
try {
int i = cursor - 1;
E previous = get(i);
lastRet = cursor = i;
return previous;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
} public int nextIndex() {
return cursor;
} public int previousIndex() {
return cursor-1;
} public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); try {
AbstractList.this.set(lastRet, e);
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
} public void add(E e) {
checkForComodification(); try {
int i = cursor;
AbstractList.this.add(i, e);
lastRet = -1;
cursor = i + 1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
private class Itr implements Iterator<E> {
/**
* Index of element to be returned by subsequent call to next.
*/
int cursor = 0; /**
* Index of element returned by most recent call to next or
* previous. Reset to -1 if this element is deleted by a call
* to remove.
*/
int lastRet = -1; /**
* The modCount value that the iterator believes that the backing
* List should have. If this expectation is violated, the iterator
* has detected concurrent modification.
*/
int expectedModCount = modCount; public boolean hasNext() {
return cursor != size();
} public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
} public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification(); try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
} final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

3.4 LinkedList 可以存储 null 值吗?元素可以重复吗?

LinkedList 底层是由双向链表实现的,并且在添加元素的时候,没有对元素进行值校验,所以可以存储 null 值,并且存储的元素是可以重复的。

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

3.5 如何边遍历 ArrayList 元素,边删除指定元素?

不支持在遍历的同时对原链表进行操作,会抛出 ConcurrentModificationException 并发修改异常,前面我们提到使用迭代器 Iterator 遍历集合时,不能对集合进行增删操作(会导致 modCount 值变化)。应该使用 Iterator 类的 remove 方法。

package com.chenpi;

import java.util.Iterator;
import java.util.LinkedList; /**
* @author 陈皮
* @version 1.0
* @description
* @date 2022/3/1
*/
public class ChenPi { public static void main(String[] args) { LinkedList<String> list = new LinkedList<>();
list.add("Java");
list.add("C++");
list.add("Python");
list.add("Lua"); Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
if ("C++".equals(next)) {
iterator.remove();
continue;
}
System.out.println(next);
} }
} // 输出结果如下
Java
Python
Lua

点击关注,第一时间了解华为云新鲜技术~

源码详解数据结构Linked List的更多相关文章

  1. 数据结构与算法系列2 线性表 使用java实现动态数组+ArrayList源码详解

    数据结构与算法系列2 线性表 使用java实现动态数组+ArrayList源码详解 对数组有不了解的可以先看看我的另一篇文章,那篇文章对数组有很多详细的解析,而本篇文章则着重讲动态数组,另一篇文章链接 ...

  2. RocketMQ源码详解 | Broker篇 · 其三:CommitLog、索引、消费队列

    概述 上一章中,已经介绍了 Broker 的文件系统的各个层次与部分细节,本章将继续了解在逻辑存储层的三个文件 CommitLog.IndexFile.ConsumerQueue 的一些细节.文章最后 ...

  3. Spark Streaming揭秘 Day25 StreamingContext和JobScheduler启动源码详解

    Spark Streaming揭秘 Day25 StreamingContext和JobScheduler启动源码详解 今天主要理一下StreamingContext的启动过程,其中最为重要的就是Jo ...

  4. spring事务详解(三)源码详解

    系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 一.引子 ...

  5. 条件随机场之CRF++源码详解-预测

    这篇文章主要讲解CRF++实现预测的过程,预测的算法以及代码实现相对来说比较简单,所以这篇文章理解起来也会比上一篇条件随机场训练的内容要容易. 预测 上一篇条件随机场训练的源码详解中,有一个地方并没有 ...

  6. [转]Linux内核源码详解--iostat

    Linux内核源码详解——命令篇之iostat 转自:http://www.cnblogs.com/york-hust/p/4846497.html 本文主要分析了Linux的iostat命令的源码, ...

  7. saltstack源码详解一

    目录 初识源码流程 入口 1.grains.items 2.pillar.items 2/3: 是否可以用python脚本实现 总结pillar源码分析: @(python之路)[saltstack源 ...

  8. Shiro 登录认证源码详解

    Shiro 登录认证源码详解 Apache Shiro 是一个强大且灵活的 Java 开源安全框架,拥有登录认证.授权管理.企业级会话管理和加密等功能,相比 Spring Security 来说要更加 ...

  9. udhcp源码详解(五) 之DHCP包--options字段

    中间有很长一段时间没有更新udhcp源码详解的博客,主要是源码里的函数太多,不知道要不要一个一个讲下去,要知道讲DHCP的实现理论的话一篇博文也就可以大致的讲完,但实现的源码却要关心很多的问题,比如说 ...

  10. Activiti架构分析及源码详解

    目录 Activiti架构分析及源码详解 引言 一.Activiti设计解析-架构&领域模型 1.1 架构 1.2 领域模型 二.Activiti设计解析-PVM执行树 2.1 核心理念 2. ...

随机推荐

  1. IEDA-Maven项目开发步骤

    1. 打开IDEA 界面,选择"New Project" 2. 输入项目名称和基本信息 输入项目名称.选择存放路径.JDK版本.以及GroupId.ArtifactId. Grou ...

  2. 使用Kali Linux进行主机发现实验

    主机发现 [实训目的] 掌握主机扫描的工作原理,学会使用ping等扫描工具,发现网络当中活跃的主机. [场景描述] 在虚拟机环境下配置4个虚拟系统"Win XP1" "W ...

  3. Ansible操作MySQL常用的几个模块

    1. mysql_user 模块 mysql_user模块用来添加,删除用户以及设置用户权限 创建MySQL数据库的用户与口令(非root@localhost用户),直接通过playbooks中的案例 ...

  4. deepin解决文件管理器打不开和桌面黑屏的问题

    总结 deepin 的优点是上手非常容易, 但截止当前(2021-6-24)的使用来说稳定性还不是非常好. 今天就遇到了无法显示桌面的问题,可以参照如下解决办法.只需使用红色框中的命令即可. 图片转载 ...

  5. Petals

    ------------恢复内容开始------------ 打开发现一堆地址冒红 滑倒后面发现E8,根据经验应该是花指令考点 然后D-->nop-->C-->P得到正常结果 然后第 ...

  6. 神经网络入门篇:详解多样本向量化(Vectorizing across multiple examples)

    多样本向量化 与上篇博客相联系的来理解 逻辑回归是将各个训练样本组合成矩阵,对矩阵的各列进行计算.神经网络是通过对逻辑回归中的等式简单的变形,让神经网络计算出输出值.这种计算是所有的训练样本同时进行的 ...

  7. Java表达式引擎选型调研分析

    1 简介 我们项目组主要负责面向企业客户的业务系统,企业的需求往往是多样化且复杂的,对接不同企业时会有不同的定制化的业务模型和流程.我们在业务系统中使用表达式引擎,集中配置管理业务规则,并实现实时决策 ...

  8. C语言一个单链表L=(a1 , a2 , … , an-1 , an),其逆单链表定义为L’=( an , an-1 , … , a2 , a1),要求逆单链表仍占用原单链表的空间。

    /* 开发者:慢蜗牛 开发时间:2020.6.11 程序功能:顺序输出和逆序输出 */ #include<stdio.h> #include<malloc.h> #define ...

  9. 通过计算巢轻松部署ROS自定义资源

    概述 阿里云资源编排服务ROS(Resource Orchestration Service)可以帮助您简化云计算资源的管理.遵循ROS定义的模板规范,您可以定义所需云计算资源的集合及资源间的依赖关系 ...

  10. Modbus 转PROFIBUS DP网关在工厂自动温度控制系统中的应用案例

    Modbus 转PROFIBUS DP 网关PM-160 在工厂自动温度控制系统中的应用案例 摘要 随着科技的发展和工业生产水平的提高,自动温度控制系统在纺织.化工.机械等各类工业控制过程中得到了广泛 ...