LinkedList详解-源码分析
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,那么将原来的头节点f的preNode设置为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变量,获取f的next节点赋值给变量next
- 将f节点内容,f节点的next节点,均赋值为null,等待GC回收
- 将next节点赋值给first
- 如果next为null的话,说明此时链表为空了,所以将尾节点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详解-源码分析的更多相关文章
- ArrayList详解-源码分析
ArrayList详解-源码分析 1. 概述 在平时的开发中,用到最多的集合应该就是ArrayList了,本篇文章将结合源代码来学习ArrayList. ArrayList是基于数组实现的集合列表 支 ...
- Shiro的Filter机制详解---源码分析
Shiro的Filter机制详解 首先从spring-shiro.xml的filter配置说起,先回答两个问题: 1, 为什么相同url规则,后面定义的会覆盖前面定义的(执行的时候只执行最后一个). ...
- Shiro的Filter机制详解---源码分析(转)
Shiro的Filter机制详解 首先从spring-shiro.xml的filter配置说起,先回答两个问题: 1, 为什么相同url规则,后面定义的会覆盖前面定义的(执行的时候只执行最后一个). ...
- Java开源生鲜电商平台-盈利模式详解(源码可下载)
Java开源生鲜电商平台-盈利模式详解(源码可下载) 该平台提供一个联合买家与卖家的一个平台.(类似淘宝购物,这里指的是食材的购买.) 平台有以下的盈利模式:(类似的平台有美菜网,食材网等) 1. 订 ...
- ArrayList、LinkedList和Vector源码分析
ArrayList.LinkedList和Vector源码分析 ArrayList ArrayList是一个底层使用数组来存储对象,但不是线程安全的集合类 ArrayList的类结构关系 public ...
- 微服务生态组件之Spring Cloud OpenFeign详解和源码分析
Spring Cloud OpenFeign 概述 Spring Cloud OpenFeign 官网地址 https://spring.io/projects/spring-cloud-openfe ...
- 微服务生态组件之Spring Cloud LoadBalancer详解和源码分析
Spring Cloud LoadBalancer 概述 Spring Cloud LoadBalancer目前Spring官方是放在spring-cloud-commons里,Spring Clou ...
- udhcp详解源码(序)
最近负责接入模块,包括dhcp.ipoe和pppoe等等.所以需要对dhcp和ppp这几个app的源代码进行一些分析.网上有比较好的文章,参考并补充自己的分析. 这篇udhcp详解是基于busybox ...
- LinkedList的实现源码分析
LinkedList 以双向链表实现.链表无容量限制,但双向链表本身使用了更多空间,也需要额外的链表指针操作. 按下标访问元素--get(i)/set(i,e) 要悲剧的遍历链表将指针移动到位(如果i ...
随机推荐
- markdownPad常用功能示例
1.列表 无序列表 姓名 张三 李四 王五 有序列表 张三 李四 王五 2.超链接 百度 3.引用 锄禾日当午,汗滴禾下土.谁知盘中餐,粒粒皆辛苦. -- 李绅<古风二首> 4.简要修饰文 ...
- MaxCompute Studio提升UDF和MapReduce开发体验
原文链接:http://click.aliyun.com/m/13990/ UDF全称User Defined Function,即用户自定义函数.MaxCompute提供了很多内建函数来满足用户的计 ...
- Gartner:2016 Q2全球服务器市场,中国再度成为亮点
对于IT产品供应商来说,Gartner.IDC等第三方分析机构市场调研报告无疑是一次"中考",成绩优异的论功行赏,迎接鲜花和掌声:差强人意,批斗会将是不可避免的,接下来加班加点,力 ...
- 02-线性结构3 Reversing Linked List
02-线性结构3 Reversing Linked List (25分) 时间限制:400ms 内存限制:64MB 代码长度限制:16kB 判题程序:系统默认 作者:陈越 单位:浙江大学 http ...
- VB中使用字典存储类对象
2019独角兽企业重金招聘Python工程师标准>>> NODE类 Public pNext As NODE Public pPrev As NODE Public data As ...
- P4168 蒲公英
神仙分块,把减写成加调了半小时.. 不过这题也启示我们其实有的分块题要把多个块的信息拿到一起维护 以前做的都是每个块的信息单独维护 写的分块题还不太多,同时维护一个块的左右边界好像有点冗余,不过这样代 ...
- Android 自定义View—清爽小巧灵活的多节点进度条
前言 最近项目有一个节点进度条的小需求,完成后,想分享出来希望可以帮到有需要的同学. 真机效果图 自定义View完整代码 开箱即用~,注释已经炒鸡详细了 /** * @description: 节点进 ...
- Git 中 “fatal: Not a valid object name: 'master'.”
报错: fatal: Not a valid object name: 'master'. 问题主要是,master并不合法,也就是没有 git commit -m "" 提交一个 ...
- rsync服务端一键安装rsync脚本(非源码)
export RSYNC_PASSWORD=123 USER=rsync AUTHUSERS=bck MK=backupmk local_dir=/backup yum remove rsync &a ...
- Blazor入门:ASP.NET Core Razor 组件
目录 关于组件 组件类 静态资产 路由与路由参数 组件参数 请勿创建会写入其自己的组参数属性的组件 子内容 属性展开 任意参数 捕获对组件的引用 在外部调用组件方法以更新状态 使用 @ 键控制是否保留 ...