链表是对上一篇博文所说的顺序表的一种实现。

与ArrayList思路截然不同,链表的实现思路是:

不同元素实际上是存储在离散的内存空间中的。

每一个元素都有一个指针指向下一个元素,这样整个离散的空间就被“串”成了一个有顺序的表。

从链表的概念来讲,它可以算是一种递归的数据结构,因为链表拿掉第一个元素剩下的部分,依然构成一个链表。

时间空间复杂度

通过索引定位其中的一个元素。由于不能像ArrayList那样直接通过计算内存地址偏移量来定位元素,只能从第一个元素开始顺藤摸瓜来数,因此为O(n)。

插入元素。实际上插入元素需要看情况:

如果指定链表中某个元素将其插之其后,那么首先得找出该元素对应的节点,还是O(n)。

如果能够直接指定节点往其后插入(如通过迭代器),那么仅仅需要移动指针即可完成,O(1)。

移除元素。移除和插入类似,得看提供的参数是什么。如果提供的是元素所在的节点,那么也只需要O(1)。

LinkedList的继承结构

首先继承结构和ArrayList类似,实现了List接口。

但是,它继承的是继承了AbstractList类的AbstractSequentialList类,

这个类的作用也是给List中的部分函数提供默认实现,只是这个类对链表这种List的实现提供了更多贴合的默认函数实现。

还有可以注意到,LinkedList实现了Deque接口,这也很显然,链表这种结构天然就适合当做双端队列使用。

LinkedList源码分析

节点定义

先来看链表的节点定义:

private static class Node {

E item;

Node next;

Node prev;

Node(Node prev, E element, Node next) {

this.item = element;

this.next = next;

this.prev = prev;

}

}

可以看到,链表节点除了保存数据外,还需要保存指向前后节点的指针。

这里,链表即有后继指针也有前驱指针,因此这是一个双向链表。

一组节点之间按顺序用指针指起来,就形成了链表的链状结构。

属性和构造函数

transient int size = 0;

transient Node first;

transient Node last;

三个属性,first和last分别指向链条的首节点和尾节点。

这样有个好处,就是链表即可以使用头插法也可以采用尾插法。

size属性跟踪了链表的元素个数。虽然说遍历一遍链表也能统计到元素个数,

但是那是O(n)的费时操作。

因此,我们可以发现链表的size方法是O(1)的时间复杂度。

public LinkedList() {

}

LinkedList的代码很简单,构造函数空空如也。

空表中,first和last字段都为null。

get和set方法

public E get(int index) {

checkElementIndex(index);

return node(index).item;

}

public E set(int index, E element) {

checkElementIndex(index);

Node x = node(index);

E oldVal = x.item;

x.item = element;

return oldVal;

}

Node node(int index) {

// assert isElementIndex(index);

if (index < (size >> 1)) {

Node x = first;

for (int i = 0; i < index; i++)

x = x.next;

return x;

} else {

Node x = last;

for (int i = size - 1; i > index; i--)

x = x.prev;

return x;

}

}

get和set的思路都是先根据索引定位到链表节点,然后获得或设置节点中的数据,这抽象出了node函数,根据索引找到链表节点。

node的思路也很显然,遍历一遍即可得到。

这里做了一点优化,我们可以发现LinkedList的实现是一个双向链表,并且LinkedList持有了头尾指针。

那么,根据索引和size就可以知道该节点是在链表的前半部分还是后半部分,

从而决定从头节点开始遍历还是从尾节点开始遍历,这样最多遍历 N / 2次即可找到。

添加/删除

public boolean add(E e) {

linkLast(e);

return true;

}

public void add(int index, E element) {

checkPositionIndex(index);

if (index == size)

linkLast(element);

else

linkBefore(element, node(index));

}

void linkLast(E e) {

final Node l = last;

final Node newNode = new Node<>(l, e, null);

last = newNode;

if (l == null)

first = newNode;

else

l.next = newNode;

size++;

modCount++;

}

void linkBefore(E e, Node succ) {

final Node pred = succ.prev;

final Node newNode = new Node<>(pred, e, succ);

succ.prev = newNode;

if (pred == null)

first = newNode;

else

pred.next = newNode;

size++;

modCount++;

}

添加/删除的思路都类似,删除的代码就不贴了。如果能够提供需要被操作的节点,就能直接移动下指针,O(1)完成。否则就需要遍历找到这个节点再操作。

需要关注两点:

有的操作是操作头指针,有的操作是操作尾指针。但是不管操作哪一个,都需要维护另外一个指针及size的值。

如果是删除,删除后及时把相关节点的item字段置为null,以帮助gc能更快的释放内存。

modCount字段分析

之前阅读ArrayList的代码时发现了modCount这一字段,它是定义在AbstractList类中的。之前不知道它起到什么作用,这次给弄明白了。

迭代器

迭代器迭代中表被修改

考虑以下这段代码:

List list = new LinkedList<>();

Iterator it = list.listIterator();

list.add(1);

it.next();

在迭代器创建之后,对表进行了修改。这时候如果操作迭代器,则会得到异常java.util.ConcurrentModificationException。

这样设计是因为,迭代器代表表中某个元素的位置,内部会存储某些能够代表该位置的信息。当表发生改变时,该信息的含义可能会发生变化,这时操作迭代器就可能会造成不可预料的事情。

因此,果断抛异常阻止,是最好的方法。

实际上,这种迭代器迭代过程中表结构发生改变的情况,更经常发生在多线程的环境中。

记录表被修改的标记

这种机制的实现就需要记录表被修改,那么思路是使用状态字段modCount。

每当会修改表的操作执行时,都将此字段加1。使用者只需要前后对比该字段就知道中间这段时间表是否被修改。

如linkedList中的头插和尾插函数,就将modCount字段自增:

private void linkFirst(E e) {

final Node f = first;

final Node newNode = new Node<>(null, e, f);

first = newNode;

if (f == null)

last = newNode;

else

f.prev = newNode;

size++;

modCount++;

}

void linkLast(E e) {

final Node l = last;

final Node newNode = new Node<>(l, e, null);

last = newNode;

if (l == null)

first = newNode;

else

l.next = newNode;

size++;

modCount++;

}

迭代器

迭代器使用该字段来判断,

private class ListItr implements ListIterator {

private Node lastReturned;

private Node next;

private int nextIndex;

private int expectedModCount = modCount;

ListItr(int index) {

// assert isPositionIndex(index);

next = (index == size) ? null : node(index);

nextIndex = index;

}

public boolean hasNext() {

return nextIndex < size;

}

public E next() {

checkForComodification();

if (!hasNext())

throw new NoSuchElementException();

lastReturned = next;

next = next.next;

nextIndex++;

return lastReturned.item;

}

/* ... */

public void set(E e) {

if (lastReturned == null)

throw new IllegalStateException();

checkForComodification();

lastReturned.item = e;

}

final void checkForComodification() {

if (modCount != expectedModCount)

throw new ConcurrentModificationException();

}

}

迭代器开始时记录下初始的值:

private int expectedModCount = modCount;

然后与现在的值对比判断是否被修改:

final void checkForComodification() {

if (modCount != expectedModCount)

throw new ConcurrentModificationException();

}

这是一个内部类,隐式持有LinkedList的引用,能够直接访问到LinkedList中的modCount字段。

java 集合modcount_源码|jdk源码之LinkedList与modCount字段相关推荐

  1. java 头尾 队列_源码|jdk源码之栈、队列及ArrayDeque分析

    栈.队列.双端队列都是非常经典的数据结构.和链表.数组不同,这三种数据结构的抽象层次更高.它只描述了数据结构有哪些行为,而并不关心数据结构内部用何种思路.方式去组织. 本篇博文重点关注这三种数据结构在 ...

  2. Java集合框架之三:HashMap源码解析

    Java集合框架之三:HashMap源码解析 版权声明:本文为博主原创文章,转载请注明出处,欢迎交流学习! HashMap在我们的工作中应用的非常广泛,在工作面试中也经常会被问到,对于这样一个重要的集 ...

  3. Java集合(超详细-含源码)

    一 集合体系结构 集合的体系结构分为单列集合和双列集合 二 Collection单列集合 Collection是单列集合的祖宗接口,他的功能是全部单列集合都可以继承使用的. 单列集合接口下又分为Lis ...

  4. java 集合反射_关于granite源码包CollectionUtil集合工具类获取集合反射类型、实例化各种集合类型HashSet/ArrayList等...

    一.前言 基于granite源码包org.granite.util.CollectionUtil集合工具类,分别获取集合反射类型java.lang.reflect.Type.实例化newCollect ...

  5. Java集合框架之接口Collection源码分析

    本文我们主要学习Java集合框架的根接口Collection,通过本文我们可以进一步了解Collection的属性及提供的方法.在介绍Collection接口之前我们不得不先学习一下Iterable, ...

  6. jdk源码(jdk源码阅读顺序)

    如何在myEclipse中查看JDK源码 myeclipse中查看jdk类库的源码步骤如下: 1.点 "window"-> "Preferences" - ...

  7. 3.Java集合-HashSet实现原理及源码分析

    一.HashSet概述: HashSet实现Set接口,由哈希表(实际上是一个HashMap实例)支持,它不保证set的迭代顺序很久不变.此类允许使用null元素 二.HashSet的实现: 对于Ha ...

  8. Java集合:ConcurrentHashMap(JDK 1.7 JDK 1.8)

    ConcurrentHashMap(JDK 1.7) HashTable容器在竞争激烈的并发环境下表现出效率低下的原因,是因为所有访问HashTable的线程都必须竞争同一把锁,那假如容器里有多把锁, ...

  9. java集合之列表:ArrayList、Vector、LinkedList

    1 package com.jdk7.chapter4; 2 3 import java.util.ArrayList; 4 import java.util.LinkedList; 5 import ...

最新文章

  1. iOS Sprite Kit教程之申请和下载证书
  2. 谷歌最新提出无需卷积、注意力 ,纯MLP构成的视觉架构
  3. WCF单元测试遇到的问题
  4. 静态NAT技术三部曲
  5. elasticsearch配置文件详解
  6. error:1407742E:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version
  7. CF666E-Forensic Examination【广义SAM,线段树合并】
  8. 如何从Java EE无状态应用程序连接到MongoDB
  9. b站在线解析_这款游戏被全B站所唾弃,每个月却依然有5000万玩家坚持在线?!...
  10. java 切换panel会闪烁_【19期】为什么Java线程没有Running状态?
  11. java running_Running
  12. 胃net的放大内镜_李锐:内镜下的早癌诊断
  13. 《大话数据结构》读后总结(八)
  14. win7自带计算机,win7系统自带的计算器不见了的解决方法
  15. linux终端安装搜狗输入法rpm,Linux下deb包安装工具(附带安装搜狗输入法)
  16. helm install Error: timed out waiting for the condition
  17. android 实现3d扫描,DIY:让Android手机轻松变3D扫描仪
  18. LinearLayout布局添加下划线
  19. 复杂场景下的权限系统该怎么玩?ABAC权限模型帮你搞定它!
  20. AV1基于机器学习的变换块快速划分

热门文章

  1. 因为一个跨域请求,我差点丢了饭碗
  2. 我那么拼命,为什么还会被裁掉?
  3. Swarm的进化和大规模应用
  4. 鸿蒙关键技术研究,鸿蒙内核源码分析(静态链接篇) | 完整小项目看透静态链接过程 | 百篇博客分析HarmonyOS源码 | v54.02...
  5. 剪映电脑版_七款手机剪辑app,效果堪比电脑软件
  6. Windows下安装ab
  7. springboot 整合mybatisplus输出sql语句不输出结果集
  8. 一分钟搭建、运行、测试SSM项目
  9. js中组装拼接json对象,通过java后端接收并解析
  10. 7 行代码优雅地实现 Excel 文件导出功能?