LinkedList详解-源码分析

LinkedList是List接口的第二个具体的实现类,第一个是ArrayList,前面一篇文章已经总结过了,下面我们来结合源码,学习LinkedList。

  • 基于双向链表实现

  • 便于插入和删除,不便于遍历

  • 非线程安全

  • 有序(链表维护顺序)

  • ...

上面是LinkedList的一些特性。

1. LinkedList类声明

源码如下所示:

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

初步分析:

  • 继承了AbstractSequentialList抽象类
  • 实现了List、Deque、Cloneable、Serializable接口

思考:

  • List、Cloneable、Serializable接口在上一篇ArrayList详解里已经分析过了,这个Deque接口是干嘛的呢?

咳咳,先谷歌一下,发现Deque的意思是双端队列,这里已经可以看出LinkedList是基于双向链表的一些端倪了,带着这点疑问,我们继续往下看。

2. 成员变量

源码如下所示:

    transient int size = 0;

    transient Node<E> first;

    transient Node<E> last;

private static final long serialVersionUID = 876323262645176354L;

比ArrayList的成员变量少了好几个呢。

初步分析:

  • size依然是集合内的元素个数
  • transient关键字标识变量不会被序列化
  • Node是节点的意思,具体代码是什么样的?

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

注意:此处只截取了Node的代码,Node是LinkedList的静态内部类,还是在LinkedList.class文件内部的

分析:

  • 声明了三个成员变量
  • 分别表示当前元素,下一个节点,上一个节点

也就是说,LinkedList的每一个元素都是一个Node,而每一个Node都储存了三部分内容,由此也就证实了LinkedList是基于双向链表的。

3. 构造方法

源码如下所示:

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

分析:

  • LinkedList提供了两个构造方法
  • 分别是对应无参构造和传入Collection子类进行构造

可以发现,相对于ArrayList,LinkedList类并没有指定容量的构造,这是为什么呢?

思考:

1. 这就是ArrayList和LinkedList底层依赖不同有关系,ArrayList底层是数组,LinkedList底层是双向链表。数组初始化是需要声明长度的,链表则不需要。

2. 传入子类进行构造时,也是调用了无参构造方法,再调用addAll()方法,将所有元素添加进去

4. 常用方法分析

addFirst(E e)

源码如下所示:

public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}

addFirst()方法是在链表头部插入一个元素,分析如下:

  • 先获取到原来的头节点,赋值给f
  • 创建一个新的节点newNode,该节点的next节点为f
  • newNode赋值给first
  • 如果原来的头节点是null的话,说明此时链表是空的,添加的是第一个元素,则将newNode也赋值给last节点
  • 如果原来的头节点不是null,那么将原来的头节点fpreNode设置为newNode
  • 链表长度加1,链表修改次数加1

add(E e)

源码如下:

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

add()方法默认是在链表的尾部进行添加元素。

分析:

  • 此处的linkLast方法是不是很眼熟?和前面的linkeFirst基本一致噢
  • 不同之处仅在于,linkFirst是对对头节点进行变更,而linkLast是对尾节点进行变更
  • 此处不再赘述

get(int index)

源码如下所示:

    public E get(int index) {
checkElementIndex(index);
return node(index).item;
}

get()方法内隐藏着LinkedList不便于进行遍历的真相!一定要搞明白哦。

分析:

  • 第一步先确认index是否在正确的范围内,范围为(0~size)
  • 第二步调用node方法返回对应索引位置的节点元素

node()方法源码如下:

    Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
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和链表长度的1/2的大小
  • 如果index小于链表长度的1/2,那么就会从头节点向index位置进行遍历,直到获取到相应节点并返回该节点
  • 如果index大于链表长度的1/2,那么从尾节点向index位置进行遍历,直到获取到相应节点并返回该节点

可以看出,当你访问的元素越靠近链表的中间,那么获取该元素所花费的时间就会越长,所以LinkedList在遍历上是比较慢的,链表本身是不支持任意性访问的,虽然LinkedList的get()方法可以读到相应元素,但是效率很低,不建议使用。

remove(Object o)

源码如下所示:

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

分析如下:

  • 元素为null时,使用==进行元素内容的判断,然后调用unlink方法
  • 元素不为null时,使用equals方法进行判断两个元素是否相同,然后调用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方法源代码有点长啊,容我慢慢道来:

  • 定义三个变量分别接收传入节点x的内容、上一个节点、下一个节点
  • 如果节点x的上一个节点为null的话,说明x节点是头节点,那么就将x节点的下一个节点赋值给头节点
  • 如果节点x不是头节点,则将x节点的下一个节点赋值给上一个节点的next节点,并将x节点的上一个节点置为null
  • 经上面两步,已经完成了x节点和上一个节点的断开,以及下一个节点和x节点的上一个节点的链接
  • 如果x节点的下一个节点为null,说明x节点是尾节点,那么就将x节点的上一个节点赋值给last节点
  • 如果x节点不是尾节点,那么将x节点的上一个节点,赋值给下一个节点的prev节点,并将x节点的下一个节点置为null
  • 经过上面几步之后,x节点就已经从链表中移除了
  • 然后将x节点的节点内容置为null,链表长度减1,修改长度记录加1
  • 返回删除节点的内容element

removeFirst()

源码如下所示:

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

分析:

  • 获取链表头节点,赋值给f
  • 如果f等于null,说明此时链表是空的,抛出异常
  • 如果f不等于null,调用unlinkFirst方法,传入f

unlinkFirst()方法源码如下所示:

    private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}

分析:

  • 获取f节点内容,赋值给element变量,获取fnext节点赋值给变量next
  • f节点内容,f节点的next节点,均赋值为null,等待GC回收
  • next节点赋值给first
  • 如果nextnull的话,说明此时链表为空了,所以将尾节点last也赋值为null
  • 否则,将next节点的prev成员变量赋值为null
  • 链表长度减1,修改记录数加1
  • 返回被移除的元素

5. 其他方法概述

LinkedList可以作为FIFO(First In First Out)的队列,也就是先进先出的队列使用,以下是关于队列的操作。

    //获取队列的第一个元素,如果为null会返回null
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//获取队列的第一个元素,如果为null会抛出异常
public E element() {
return getFirst();
}
//获取队列的第一个元素,如果为null会返回null
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//获取队列的第一个元素,如果为null会抛出异常.
public E remove() {
return removeFirst();
}
//将元素添加到队列尾部
public boolean offer(E e) {
return add(e);
}

LinkedList也可以作为栈使用,栈的特性是LIFO(Last In First Out),也就是后进先出。 添加和删除元素都只操作队列的首节点即可。

源码如下:

	public boolean offerFirst(E e) {
addFirst(e);
return true;
} public boolean offerLast(E e) {
addLast(e);
return true;
} public E peekFirst() {
final Node<E> f = first;
return (f == null) ? null : f.item;
} public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
} public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
} public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
} public void push(E e) {
addFirst(e);
} public E pop() {
return removeFirst();
} public boolean removeFirstOccurrence(Object o) {
return remove(o);
} public boolean removeLastOccurrence(Object o) { if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
if (x.item == null) {
//调用unlink方法删除指定节点
unlink(x);
return true;
}
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}

6. 总结

LinkedList相对于ArrayList而言,源码并没有很复杂,从源码中我们得知了以下相关信息:

  • LinkedList是基于双向链表实现的,即每一个节点都保存了上一个节点和下一个节点的信息
  • LinkedList根据索引获取元素效率低的原因是因为它需要一个节点一个节点的遍历,获取首节点和尾节点很快
  • LinkedList实现了Deque接口,具有双向队列的性质,可以实现数据结构中的堆栈。
  • ...

知之为知之,不知为不知,是知也。

LinkedList详解-源码分析的更多相关文章

  1. ArrayList详解-源码分析

    ArrayList详解-源码分析 1. 概述 在平时的开发中,用到最多的集合应该就是ArrayList了,本篇文章将结合源代码来学习ArrayList. ArrayList是基于数组实现的集合列表 支 ...

  2. Shiro的Filter机制详解---源码分析

    Shiro的Filter机制详解 首先从spring-shiro.xml的filter配置说起,先回答两个问题: 1, 为什么相同url规则,后面定义的会覆盖前面定义的(执行的时候只执行最后一个). ...

  3. Shiro的Filter机制详解---源码分析(转)

    Shiro的Filter机制详解 首先从spring-shiro.xml的filter配置说起,先回答两个问题: 1, 为什么相同url规则,后面定义的会覆盖前面定义的(执行的时候只执行最后一个). ...

  4. Java开源生鲜电商平台-盈利模式详解(源码可下载)

    Java开源生鲜电商平台-盈利模式详解(源码可下载) 该平台提供一个联合买家与卖家的一个平台.(类似淘宝购物,这里指的是食材的购买.) 平台有以下的盈利模式:(类似的平台有美菜网,食材网等) 1. 订 ...

  5. ArrayList、LinkedList和Vector源码分析

    ArrayList.LinkedList和Vector源码分析 ArrayList ArrayList是一个底层使用数组来存储对象,但不是线程安全的集合类 ArrayList的类结构关系 public ...

  6. 微服务生态组件之Spring Cloud OpenFeign详解和源码分析

    Spring Cloud OpenFeign 概述 Spring Cloud OpenFeign 官网地址 https://spring.io/projects/spring-cloud-openfe ...

  7. 微服务生态组件之Spring Cloud LoadBalancer详解和源码分析

    Spring Cloud LoadBalancer 概述 Spring Cloud LoadBalancer目前Spring官方是放在spring-cloud-commons里,Spring Clou ...

  8. udhcp详解源码(序)

    最近负责接入模块,包括dhcp.ipoe和pppoe等等.所以需要对dhcp和ppp这几个app的源代码进行一些分析.网上有比较好的文章,参考并补充自己的分析. 这篇udhcp详解是基于busybox ...

  9. LinkedList的实现源码分析

    LinkedList 以双向链表实现.链表无容量限制,但双向链表本身使用了更多空间,也需要额外的链表指针操作. 按下标访问元素--get(i)/set(i,e) 要悲剧的遍历链表将指针移动到位(如果i ...

随机推荐

  1. Vagrant (二) - 日常操作

    立即上手 上一节中,我们介绍了怎样安装 Vagrant,安装本身并不困难.本章节中我们首先要快速上手,以便获得一个直观的概念: 建立一个工作目录 打开命令行工具,终端工具,或者iTerm2等,建立一个 ...

  2. mybatis if test标签的使用

    2019独角兽企业重金招聘Python工程师标准>>> 在使用mybatis 有时候需要进行判断的. 而我们知道mybatis获取值有两种方式 #{}和${}的. 那么,在mybat ...

  3. 由JS数组去重说起

    一.问题描述: var array=[1,45,3,1,4,67,45],请编写一个函数reDup来去掉其中的重复项,即 reDup(array); console.log(array);//[1,4 ...

  4. Codeforces Round #509 (Div. 2) A. Heist 贪心

    There was an electronic store heist last night. All keyboards which were in the store yesterday were ...

  5. The Preliminary Contest for ICPC Asia Xuzhou 2019 徐州网络赛 B so easy

    题目链接:https://nanti.jisuanke.com/t/41384 这题暴力能过,我用的是并查集的思想,这个题的数据是为暴力设置的,所以暴力挺快的,但是当他转移的点多了之后,我觉得还是我这 ...

  6. linux 服务器/客户端 tcp通信的简单例子

    昨天弄了sublime之后没有弄输入中文的了,学生党来着,刚好可以练练英语(英语渣渣,还要考六级),所以注释都写英文的(语法什么的就别太深究了) 服务器端: /*start from the very ...

  7. 04 全局局部配置 wxml数据绑定 事件 冒泡

    一. 配置介绍 一个小程序应用程序会包括最基本的两种配置文件.一种是全局的 app.json 和 页面自己的 page.json(index.json /test.json等) 注意:配置文件中不能出 ...

  8. vue跳转的两种方法

    1 标签跳转 <router-link to='two'><button>点我到第二个页面</button></router-link> 2 点击事件跳 ...

  9. 【面试题】String类、包装类的不可变性

    不可变类的意思是创建该类的实例后,该实例的实例变量是不可改变的.Java提供的8个包装类和String类都是不可变类.因此String和8个包装类都具有不可变性. 就拿String类来说,通过阅读St ...

  10. redux中间件的理解

    redux的中间件就是用来处理reducer和actions之间应用,常用的中间件有redux-thunk,redux-sage.在redux中通过applyMiddleware方法使用中间件 使用例 ...