一:类的继承关系


我们看下类的继承关系
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. hadoop/hbase/hive单机扩增slave

    原来只有一台机器,hadoop,hbase,hive都安装在一台机器上,现在又申请到一台机器,领导说做成主备, 要重新配置吗?还是原来的不动,把新增的机器做成slave,原来的当作master?网上找 ...

  2. 第六章 组件 59 组件切换-使用Vue提供的component元素实现组件切换

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8&quo ...

  3. static和assets的区别

    assets和static两个都是用于存放静态资源文件. 放在static中的文件不会进行构建编译处理,也就不会压缩体积,在打包时效率会更高,但体积更大在服务器中就会占据更大的空间 放在assets中 ...

  4. Java-Base64工具类

    /* * Base64 encoding and decoding. * Copyright (C) 2001-2004 Stephen Ostermiller * http://ostermille ...

  5. 点双联通分量(BCC)的正确姿势

    Tarjan求点双连通分量 - 李昊哲

  6. @Valid与@Validated

    Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR- ...

  7. 【转】分布式文件系统FastDFS原理介绍

    什么是FastDFS? FastDFS是一个开源的轻量级分布式文件系统.它解决了大数据量存储和负载均衡等问题.特别适合以中小文件(建议范围:4KB < file_size <500MB)为 ...

  8. App自动化测试介绍

  9. 2019南昌网络赛 J Distance on the tree 主席树+lca

    题意 给一颗树,每条边有边权,每次询问\(u\)到\(v\)的路径中有多少边的边权小于等于\(k​\) 分析 在树的每个点上建\(1​\)到\(i​\)的权值线段树,查询的时候同时跑\(u,v,lca ...

  10. react富文本编辑器

    首先安装两个插件 yarn add react-draft-wysiwyg draftjs-to-html --save 使用的代码如下 import React from 'react' impor ...