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 ...
随机推荐
- webug3.0靶场渗透基础Day_2(完)
第八关: 管理员每天晚上十点上线 这题我没看懂什么意思,网上搜索到就是用bp生成一个poc让管理员点击,最简单的CSRF,这里就不多讲了,网上的教程很多. 第九关: 能不能从我到百度那边去? 构造下面 ...
- ansible的剧本play(四)
aaarticlea/png;base64,iVBORw0KGgoAAAANSUhEUgAAA68AAAETCAYAAADZDzDOAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjw
- POJ 1845-Sumdiv(厉害了这个题)
Description Consider two natural numbers A and B. Let S be the sum of all natural divisors of A^B. D ...
- dij-spfa乱搞
以前见过一篇另类堆优化dij的题解,然而找不到了 那位作者称它为dij-spfa(大概是这个意思,然而确实很形象 这方法比较玄学,正确性没有严格证出来,然而对拍是验证猜想的最好途径 不过也可能并不玄学 ...
- 10.秋招复习简单整理之CSMA/CD协议
我们知道,总线上只要有一台计算机在发送数据,总线的传输资源就被占用.因此,在同一时间只能允许一台计算机发送数据,否则各计算机之间就会相互干扰,使得所发送的数据被破坏.因此,如何协调总线上各计算机的工作 ...
- libevent(九)bufferevent
bufferevent,带buffer的event struct bufferevent { struct event_base *ev_base; const struct bufferevent_ ...
- thinkphp5.x系列 RCE总结
Thinkphp MVC开发模式 执行流程: 首先发起请求->开始路由检测->获取pathinfo信息->路由匹配->开始路由解析->获得模块.控制器.操作方法调度信息 ...
- E. Yet Another Task with Queens(分类思想)
\(\color{Red}{描述}\) \(在n*n的棋盘上有m个K皇后,每个皇后可能被来自8个方向的其他皇后攻击\) \(每个皇后只可能被(0-8)只皇后攻击,分别求出被(0-8)只皇后攻击的皇后数 ...
- 单调队列+二分 G - Queue 小阳买水果
B. Queue 这个题目会做的很偶然,突然想到的,因为我们要求离这只海象的最远的比他年轻的海象,这个年轻的海象可以用单调栈维护. 就是从前往后遍历一遍,单调栈里面存年龄从小往大的海象,这个为什么这么 ...
- HC32F003C4PA GPIO Output
1.打开启动文件,找到并跳转至SystemInit函数 void SystemInit(void) { stc_clk_systickcfg_t stcCfg; // TODO load trim f ...