LinkedList 是链表的经典实现,其底层采用链表节点的方式实现。

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

从类继承结构图可以看到,LinkedList 不仅实现了 List 接口,还实现了 Deque 双向队列接口。

原理

为了深入理解 LinkedList 的原理,我们将从类成员变量、构造方法、核心方法两个方面逐一介绍。

类成员变量

// 链表大小
transient int size = 0;
// 首节点
transient Node<E> first;
// 尾节点
transient Node<E> last;
// Node节点
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 总共有 2 个构造方法:

public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}

构造方法比较简单,这里不深入介绍。

核心方法

在 LinkedList 中最为核心的是查找、插入、删除、扩容这几个方法。

查找

LinkedList 底层基于链表结构,无法向 ArrayList 那样随机访问指定位置的元素。LinkedList 查找过程要稍麻烦一些,需要从链表头结点(或尾节点)向后查找,时间复杂度为 O(N)。相关源码如下:

public E get(int index) {
checkElementIndex(index);
return node(index).item;
} Node<E> node(int index) {
/*
* 如果获取的元素小于容量的一般,则从头结点开始查找,否则从尾节点开始查找。
*/
if (index < (size >> 1)) {
Node<E> x = first;
// 循环向后查找,直至 i == index
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;
}
}

上面的代码比较简单,主要是通过遍历的方式定位目标位置的节点。获取到节点后,取出节点存储的值返回即可。这里面有个小优化,即通过比较 index 与节点数量 size/2 的大小,决定从头结点还是尾节点进行查找。

插入

LinkedList 除了实现了 List 接口相关方法,还实现了 Deque 接口的很多方法,例如:addFirst、addLast、offerFirst、offerLast 等。但这些方法的实现思路大致都是一样的,所以我只讲 add 方法的实现。

add 方法有两个方法,一个是直接插入队尾,一个是插入指定位置。

我们先来看第一个add方法:直接插入队列。

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

可以看到其直接调用了 linkLast 方法,其实它就是 Deque 接口的一个方法。

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

上述代码进行了节点的创建以及引用的变化,最后增加链表的大小。

我们继续看第二个add方法:插入指定位置。

public void add(int index, E element) {
checkPositionIndex(index); if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}

如果我们插入的位置还是链表尾部,那么还是会调用 linkLast 方法。否则调用 node 方法取出插入位置的节点,否则调用 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++;
}

上述代码进行了节点的创建以及引用的变化,最后增加链表的大小。

删除

删除节点有两个方法,第一个是移除特定的元素,第二个是移除某个位置的元素。

我们先看第一个删除方法:移除特定的元素。

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

上述代码的大致思路为:遍历找到删除的节点,之后调用 unlink() 方法解除引用。我们继续看看 unlink() 方法的代码。

E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev; if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
} if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
} x.item = null;
size--;
modCount++;
return element;
}

unlink() 代码里就是做了一系列的引用修改操作。下面的步骤图非常详细地解释了整个删除过程。

本文图片来源于田小波的博客

总结

经过上面的分析,我们可以知道 LinkedList 有如下特点:

  • 底层基于链表实现,修改速度快,读取速度慢(读取时间复杂度O(N),修改时间复杂度O(N),因为要查找元素,所以修改也是O(N))。
  • 非线程安全。
  • 与 ArrayList 不同,LinkedList 没有容量限制,所以也没有扩容机制。

集合系列 List(四):LinkedList的更多相关文章

  1. Java 集合系列05之 LinkedList详细介绍(源码解析)和使用示例

    概要  前面,我们已经学习了ArrayList,并了解了fail-fast机制.这一章我们接着学习List的实现类——LinkedList.和学习ArrayList一样,接下来呢,我们先对Linked ...

  2. 【转】Java 集合系列05之 LinkedList详细介绍(源码解析)和使用示例

    概要  前面,我们已经学习了ArrayList,并了解了fail-fast机制.这一章我们接着学习List的实现类——LinkedList.和学习ArrayList一样,接下来呢,我们先对Linked ...

  3. Java 集合系列(四)—— ListIterator 源码分析

    以脑图的形式来展示Java集合知识,让零碎知识点形成体系 Iterator 对比   Iterator(迭代器)是一种设计模式,是一个对象,用于遍历集合中的所有元素.  Iterator 包含四个方法 ...

  4. 【Java集合系列二】LinkedList解析

    一.简介 1.LinkedList继承关系 2.LinkedList底层实现 LinkedList使用双向链表存储数据,所以没有默认的容量,也不会有扩容一说.只有两个指针,永远指向链表的两端:firs ...

  5. Java 集合系列08之 List总结(LinkedList, ArrayList等使用场景和性能分析)

    概要 前面,我们学完了List的全部内容(ArrayList, LinkedList, Vector, Stack). Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例 Ja ...

  6. 【转】Java 集合系列08之 List总结(LinkedList, ArrayList等使用场景和性能分析)

    概要 前面,我们学完了List的全部内容(ArrayList, LinkedList, Vector, Stack). Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例 Ja ...

  7. 【Java集合系列】目录

    2017-07-29 13:49:40 一.Collection的全局继承关系 二.系列文章 [Java集合系列一]ArrayList解析 备注: 1.ArrayList本质上就是一个数组,所有对外提 ...

  8. Java 集合系列目录(Category)

    下面是最近总结的Java集合(JDK1.6.0_45)相关文章的目录. 01. Java 集合系列01之 总体框架 02. Java 集合系列02之 Collection架构 03. Java 集合系 ...

  9. Java 集合系列 07 List总结(LinkedList, ArrayList等使用场景和性能分析)

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  10. Java 集合系列 04 LinkedList详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

随机推荐

  1. Spring boot 集成 阿里 Mqtt

    因为公司业务需求,需要接入 阿里Mqtt,自己基于Spring写了一个小demo,记录下来,已备以后需要. 第一步 创建一个实体bean用来装载 MqttClient private MqttClie ...

  2. vue 页面间使用路由传参数,刷新页面后获取不到参数的问题

    情况 情况再简单说明一下: 有三个页面(a-列表页面,b-内页1,c-内页2),页面a->页面b->页面c有参数传递.从a进入b后,刷新b页面拿不到a页面传进来的参数.或者b页面再进入c页 ...

  3. JS-常考算法题解析

    常考算法题解析 这一章节依托于上一章节的内容,毕竟了解了数据结构我们才能写出更好的算法. 对于大部分公司的面试来说,排序的内容已经足以应付了,由此为了更好的符合大众需求,排序的内容是最多的.当然如果你 ...

  4. 《Java基础知识》Java字符串详解

    本文内容: String类的概述 String类的使用步骤 String类的常用方法 本文目的: 能够使用String类的构造方法创建字符串对象 能够明确String类的构造方法创建对象,和直接赋值创 ...

  5. 基于C#WPF框架——动画

    WPF提供了一个更高级的模型,通过该模型可以只关注动画的定义,而不必考虑它们的渲染方式.这个模型基于依赖项属性基础架构.本质上,WPF动画只不过是在一段时间间隔内修染方式.这个模型基于依赖项属性基础架 ...

  6. Vue与Django前后台分离跨域配置

    一.跨域: 简单来说:如果前端向后端请求数据,前后端的的ip和端口都是不一致的,就是不在统一域名下,就出现了CORS跨域问题. 二.后台处理跨域 在django后台环境目录下安装插件: >: p ...

  7. Android 状态栏通知 Notification

    private NotificationManager manager; private Notification.Builder builder; @Override protected void ...

  8. Android 线性布局 计算器

    <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android=&quo ...

  9. Android JSON解析插件

    JSON是一种轻量级的数据格式,用于数据的交互. Android交互数据主要两种方式:JSON和 XML.XML格式比JSON格式数量略大,所以大多都使用Json数据格式. 在Android开发的过程 ...

  10. 《Hands-On System Programming with Go》之读文件

    有点全,但不是很全. 一次读入,分批次读入,缓存读入. 要记得这几种不同读取的应用场景. package main import ( "bufio" "bytes&quo ...