一:类的继承关系


我们看下类的继承关系
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable

继承抽象的AbstractSequentiaList类,供了一个基本的List接口实现,为实现序列访问的数据储存结构的提供了所需要的最小化的接口实现。.

同时LinkedList也实现了Cloneable、java.io.Serializable、Deque(双端队列)等接口


二:类的成员属性
1.成员属性比较简单
/**
* 当前链表长度
*/
transient int size = 0;

/**
* 头结点
*/
transient Node<E> first;

/**
* 尾结点
*/
transient Node<E> last;

2.这里我们要看Node这个实体类,因为java是没有指针这一说的,所以用Node自己来模拟实现
/**
* 静态内部类,它的创建是不需要依赖于外围类,可以被实例化
* @param <E>
*/
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;
}
}

分析:我们知道java的LinkedList是个双向链表,java没有指针,那么是怎么找到前一个元素结点和后一个元素结点呢?从上面的代码很容易就可以看出,其实就是在Node类的成员属性下直接记录了下一个结点Node<E> next 和 上一个结点 Node<E> prev,从而达到模拟指针的效果。



三:构造方法
1.不带参的构造方法,啥也没做
/**
* Constructs an empty list.
*/
public LinkedList() {
}


2.带参构造方法
/**
* 带参构造方法,传入集合
*/
public LinkedList(Collection<? extends E> c) {
//构造方法
this();
//添加方法
addAll(c);
}

分析:这里将集合直接传入addAll()方法


/**
* 参数为前链表长度和集合
*/
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}

分析:这里传入了当前链表长度和集合


/**
* 从索引为index结点的尾部,开始插入所有集合的元素
*/
public boolean addAll(int index, Collection<? extends E> c) {
//检验长度是否在链表长度区间
checkPositionIndex(index);
//将集合转为数组
Object[] a = c.toArray();
//数组长度
int numNew = a.length;
//如果等于0直接返回false
if (numNew == 0)
return false;

//两个记录结点
Node<E> pred, succ;
//如果当前传入的长度等于链表当前最大长度
if (index == size) {
//succ结点等于null
succ = null;
//pred结点等于尾结点
pred = last;
} else {
//返回index索引的结点
succ = node(index);
//pred记录等于index索引结点的上一个结点
pred = succ.prev;
}

//遍历数组
for (Object o : a) {
//将值强转为E类型
@SuppressWarnings("unchecked") E e = (E) o;
//每次循环new一个newNode结点,newNode结点的值等于e,newNode上一结点prev等于pred记录结点,newNode下一节点等于null
//其实这个newNode就是pred的下一个结点,因为newNode的上一节点等于pred
Node<E> newNode = new Node<>(pred, e, null);
//如果当前记录结点pred等于null
if (pred == null)
//则头结点等于newNode
first = newNode;
else
//当前记录pred的下一节点等于newNode,相当于把succ记录结点给覆盖了
pred.next = newNode;
//当前记录结点更新为newNode,也就是相当于链表往后移动一位
pred = newNode;
}

if (succ == null) {
//尾结点等于当前记录结点pred
last = pred;
} else {
//重新把pred结点下一个结点赋值为succ记录结点
pred.next = succ;
//并且让succ记录结点的上一个结点等于最新的pred记录结点
succ.prev = pred;
}

//链表的长度加上集合的长度
size += numNew;
//修改次数加1
modCount++;
return true;
}

分析:首先我们要知道这个addAll()方法是从索引为index结点开始向后插入元素的,链表的索引是从0开始的。那么到底是怎么插入元素的呢?我们来逐步分析下:

//检验长度是否在链表长度区间
checkPositionIndex(index);
//将集合转为数组
Object[] a = c.toArray();
//数组长度
int numNew = a.length;
//如果等于0直接返回false
if (numNew == 0)
return false;

分析:这里这先校验下传入的index长度是否在链表区间(0到size),然后将集合转为数组,如果数组长度等于0的话,直接返回了。


//两个记录结点
Node<E> pred, succ;
//如果当前传入的索引等于链表当前最大长度
if (index == size) {
//succ结点等于null
succ = null;
//pred结点等于尾结点
pred = last;
} else {
//返回index索引的结点
succ = node(index);
//pred记录等于index索引结点的上一个结点
pred = succ.prev;
}

分析:两个记录结点Node<E> pred,succ非常重要,它是为了方便我们我们操作链表的。

如果,当前传进来的索引index等于链表长度size,那么succ结点等于null,为什么呢?因为index=size下标已经超出链表了,所以值肯定为null了。也因此,我们可以理解succ其实就是索引为index的结点,也就是我们要开始插入元素的结点,然后pred结点等于尾结点。从这可以看出,我们前面构造方法传入当前链表长度和集合是从链表的尾部开始添加的(也就是尾结点的下一个结点),符合逻辑。

如果当前传入的索引不大于链表长度size,succ等于index索引的结点,pred还是等于index索引的上一个结点。我们看下这个node(index)方法,看完之后我们明白它确实是返回index索引的结点,然后遍历的时候有个小技巧,如果索引小于链表长度的一半从头结点开始往后遍历,否则从尾结点往前开始遍历,节省时间。
/**
* 传入index长度,链表返回索引为index的node结点(注意这里的链表索引也是从0开始)
* 如果传入的长度小于链表长度一遍,那么从链表头结点开始遍历
* 如果传入的长度大于或等于链表长度的一半,那么从链表的尾结点开始遍历
*/
Node<E> node(int index) {
//如果传入的长度小于链表长度的一半(size >> 1链表长度除以2,这种写法运算速度稍快,可以根据实际需求应用)
if (index < (size >> 1)) {
//当前记录结点等于头结点
Node<E> x = first;
//从下表为0开始遍历到索引为index-1
//这里注意下从0到index-1需要迭代7次
for (int i = 0; i < index; i++)
//当前记录结点等于下一节点
x = x.next;
//所以返回的是索引为indx的点!!
return x;
} else {
//如果传入的长度大于或者等于当前链表长度的一半
//当前记录结点等于尾结点
Node<E> x = last;
//从链表末尾开始往前遍历
for (int i = size - 1; i > index; i--)
//当前记录结点等于上一节点
x = x.prev;
//返回索引为index的点!
return x;
}
}
到此我们搞明白了,
//两个记录结点
Node<E> pred, succ;

这两个结点的含义,succ代表当前传入索引index的结点,pred代表索引index结点的上一个结点,这两个结点是为了我们方便操作链表。


接下来,我们继续往下分析:
//遍历数组
for (Object o : a) {
//将值强转为E类型
@SuppressWarnings("unchecked") E e = (E) o;
//每次循环new一个newNode结点,newNode结点的值等于e,newNode上一结点prev等于pred记录结点,newNode下一节点等于null
//其实这个newNode就是pred的下一个结点,因为newNode的上一节点等于pred
Node<E> newNode = new Node<>(pred, e, null);
//如果当前记录结点pred等于null
if (pred == null)
//则头结点等于newNode
first = newNode;
else
//当前记录pred的下一节点等于newNode,相当于把succ记录结点给覆盖了
pred.next = newNode;
//当前记录结点更新为newNode,也就是相当于链表往后移动一位
pred = newNode;
}

分析:前面将集合转为了数组,这里遍历数组,每次循环将遍历的值转为类型E(泛型)。new一个名为newNode的结点,构造函数初始化,newNode结点的上一结点等于pred结点(pred代表索引index结点的上一个结点),newNode结点的值为e,newNode结点的下一节点为null。看懂了没,哈哈,其实这个newNode相当于“覆盖”了succ结点(当前索引为index的结点),这里画个图给大家理解下吧。

newNode结点的上一结点等于pred结点,下一节点此时为null

if (pred == null)
//则头结点等于newNode
first = newNode;
else
//当前记录pred的下一节点等于newNode,相当于把succ记录结点给覆盖了
pred.next = newNode;
当pred == null时,链表的头结点等于newNode。因为如果当前传入索引index的结点succ为头结点,那么上一个结点pred就肯定会null,如下图:
pred不等于null时,pred结点的下一节点更改为newNode结点,如下图:
然后pred.next = newNode也就是当前传入索引index的结点succ的上一个结点变为了newNode结点,以便下次再添加时,pred变为newNode,for循环一次结束,第二次循环的话,添加效果图如下:

如此,直至for循环结束。


然后,再往下看代码
if (succ == null) {
//尾结点等于当前记录结点pred
last = pred;
} else {
//重新把pred结点下一个结点赋值为succ记录结点
pred.next = succ;
//并且让succ记录结点的上一个结点等于最新的pred记录结点
succ.prev = pred;
}

分析:当传入的index等于链表长度size时,链表索引是从0开始的,所以超出链表索引,那么当前索引succ == null,则直接让pred等于尾结点(很好理解吧?succ的上一个结点不就是索引为index-1的尾结点)。如果不等于null,把pred(此时的pred等于最后一个newNode结点)下一个结点指向succ结点,succ的上一个结点指向pred,效果如下图:


最后:
//链表的长度加上集合的长度
size += numNew;
//修改次数加1
modCount++;
return true;

分析:链表长度加上集合的长度,修改次数加1,返回


到此,addAll()方法分析结束了,总结下,我们明白了在添加链表元素时并不是真正所谓上的“添加”,这里的添加其实是改变指针的指向从而达到往链表添加的目的,删除链表元素也类似。


四:我们看下最常用的几个方法
1.往链表末尾添加单个元素方法
/**
* 往链表末尾添加元素
*/
public boolean add(E e) {
linkLast(e);
return true;
}

分析:传入元素的值,调用linkLast()方法

/**
* Links e as last element.
*/
void linkLast(E e) {
//尾结点
final Node<E> l = last;
//new一个新结点,上一个结点等于尾结点,结点值等于e,下一个结点等于null
final Node<E> newNode = new Node<>(l, e, null);
//尾结点等于新结点
last = newNode;
//链表没有元素,则尾结点为null
if (l == null)
//让头结点等于新结点
first = newNode;
else
//否则,尾结点的下一个结点等于新结点
l.next = newNode;
//链表长度加1
size++;
//修改次数加1
modCount++;
}

分析:这里添加和上面思想其实是一致的,所以就不详解了和画图了,直接看上面我写的注释。


2.根据索引删除元素
/**
* 根据索引删除结点
*/
public E remove(int index) {
//检查索引是否越界
checkElementIndex(index);
//node(index)索引为index的node结点,传入unLink()方法
return unlink(node(index));
}

继续往下看unlink()方法,传入的是索引为index的node结点对象
/**
* 删除指定节点元素
*/
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;

//如果当前节点的上一节点prev为null
if (prev == null) {
//头节点等于下一节点
first = next;
} else {
//上一节点成员变量下一节点改为next
prev.next = next;
//当前节点的上一节点置为null方便回收
x.prev = null;
}

//如果当前节点的下一节点next为null(也就是链表最后一个元素)
if (next == null) {
//链表的尾节点等于当前节点的上一节点prev
last = prev;
} else {
//当前节点的下一节点next的成员变量prev等于当前节点的上一节点prev
next.prev = prev;
//当前节点的下一节点置为null
x.next = null;
}

//当前节点的值置为null
x.item = null;
//当前链表长度减1
size--;
//修改次数加1
modCount++;
//返回当前节点元素值
return element;
}

分析:这里的删除指定节点,其实道理也是类似,通过改变上一节点和下一节点各自的成员变量引用从而达到删除当前元素节点的效果。






到此为止,LinkedList其它常用方法源码不再进行分析了,基本掌握了核心的原理,其它的都是类似。
这样我们就简单的看了下LinkedList源码。
下一篇分析HashMap源码

jdk1.8-LinkedList源码分析的更多相关文章

  1. Java入门系列之集合LinkedList源码分析(九)

    前言 上一节我们手写实现了单链表和双链表,本节我们来看看源码是如何实现的并且对比手动实现有哪些可优化的地方. LinkedList源码分析 通过上一节我们对双链表原理的讲解,同时我们对照如下图也可知道 ...

  2. ArrayList 和 LinkedList 源码分析

    List 表示的就是线性表,是具有相同特性的数据元素的有限序列.它主要有两种存储结构,顺序存储和链式存储,分别对应着 ArrayList 和 LinkedList 的实现,接下来以 jdk7 代码为例 ...

  3. Java集合之LinkedList源码分析

    概述 LinkedLIst和ArrayLIst一样, 都实现了List接口, 但其内部的数据结构不同, LinkedList是基于链表实现的(从名字也能看出来), 随机访问效率要比ArrayList差 ...

  4. java集合系列之LinkedList源码分析

    java集合系列之LinkedList源码分析 LinkedList数据结构简介 LinkedList底层是通过双端双向链表实现的,其基本数据结构如下,每一个节点类为Node对象,每个Node节点包含 ...

  5. HashMap实现原理(jdk1.7),源码分析

    HashMap实现原理(jdk1.7),源码分析 ​ HashMap是一个用来存储Key-Value键值对的集合,每一个键值对都是一个Entry对象,这些Entry被以某种方式分散在一个数组中,这个数 ...

  6. Java集合基于JDK1.8的LinkedList源码分析

    上篇我们分析了ArrayList的底层实现,知道了ArrayList底层是基于数组实现的,因此具有查找修改快而插入删除慢的特点.本篇介绍的LinkedList是List接口的另一种实现,它的底层是基于 ...

  7. LinkedList源码分析(jdk1.8)

    LinkedList概述 ​ LinkedList 是 Java 集合框架中一个重要的实现,我们先简述一下LinkedList的一些特点: LinkedList底层采用的双向链表结构: LinkedL ...

  8. LinkedList(JDK1.8)源码分析

    双向循环链表 双向循环链表和双向链表的不同在于,第一个节点的pre指向最后一个节点,最后一个节点的next指向第一个节点,也形成一个"环".而LinkedList就是基于双向循环链 ...

  9. List中的ArrayList和LinkedList源码分析

    ​ List是在面试中经常会问的一点,在我们面试中知道的仅仅是List是单列集合Collection下的一个实现类, List的实现接口又有几个,一个是ArrayList,还有一个是LinkedLis ...

  10. LinkedList 源码分析(JDK 1.8)

    1.概述 LinkedList 是 Java 集合框架中一个重要的实现,其底层采用的双向链表结构.和 ArrayList 一样,LinkedList 也支持空值和重复值.由于 LinkedList 基 ...

随机推荐

  1. 需要以管理员的身份运行程序(winform)

    1.添加应用程序清单文件(app.manifest) 2.打开app.manifest,将<requestedExecutionLevel level="asInvoker" ...

  2. 重装maven

    重装原因:换旧版本! 一.直接删除maven文件夹 二.直接删除本地仓库文件 三.删除之前相关的项目(不是必须) 四.下载一个新的,网址 https://maven.apache.org/downlo ...

  3. 生产问题之StackOverflowError异常小记

    印象中,这是项目中首次遇到 StackOverflowError,在此做个笔记. (吃饭回来写) …… 吃饭回来后,尝试把代码回退来分析异常产生原因,然而却无法复现这个栈溢出异常……醉了 有缘再见再写 ...

  4. React全家桶构建一款Web音乐App实战(六):排行榜及歌曲本地持久化

    上一节使用Redux管理歌曲相关数据,实现核心播放功能,播放功能是本项目最复杂的一个功能,涉及各个组件之间的数据交互,播放逻辑控制.这一节继续开发排行榜列表和排行榜详情,以及把播放歌曲和播放歌曲列表的 ...

  5. 原生 JS实现一个简单分页插件

    最近做的一个 PC端的需求,这个需求中有一个小点,页面底部有一块列表区域,这个列表的数据量比较大,需要进行分页控制,切换页码的时候,发送一个 ajax请求,在页面无刷新的情况下,实现列表数据的刷新,所 ...

  6. PyHook3----键盘鼠标操作

    需要安装 pywin32 安装pyHook 下载网址:https://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml 注意选择的pyHook版本一定要和python对 ...

  7. 四 java web考点

    一.GET和POST区别(参考Servlet&JSP学习笔记) <form>中method属性默认为GET. 1.使用POST的情况 GET跟随URL之后,请求参数长度有限,过长的 ...

  8. SpringBoot常见报错及解决方法

    问题一 Field redisUtil in com.sui.demo.controller.redisController required a bean of type 'com.sui.demo ...

  9. 浏览器表单自动填充默认样式 - autofill问题解决

    这不知道是个什么神仙网站,解决办法写的这么清楚,页面有这么让人舒服 https://lhajh.github.io/css/chrome/2018/04/17/The-chrome-browser-f ...

  10. win.10 禁止自动更新

             ·  Windows 10:“我已经更新完毕,请重启我吧主人!” ·  Windows 10:“好吧,主人在忙,我重启了先~” ·  Windows 10: “正在配置windows ...