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 ...
随机推荐
- 2019-2020-1 20199325《Linux内核原理与分析》第九周作业
第九周作业要求: 理解Linux系统中进程调度的时机,可以在内核代码中搜索schedule()函数,看都是哪里调用了schedule(),判断我们课程内容中的总结是否准确: 使用gdb跟踪分析一个sc ...
- [Linux] 检查是否已有进程在运行
出处:sblim-sfcb-1.4.9 / sfcBroker.c int process_is_running() { #define STRBUF_LEN 512 #define BUF_LEN ...
- [Qt] 打开文件夹 Windows
bool ok = QDesktopServices::openUrl(QUrl("c:/users/administrator/desktop/dir"));
- java中functional interface的分类和使用
目录 简介 Functional Interface Function:一个参数一个返回值 BiFunction:接收两个参数,一个返回值 Supplier:无参的Function Consumer: ...
- mac 关闭系统完整性保护 SIP(System Integrity Protection)的方法
在 OS X El Capitan 中有一个跟安全相关的模式叫 SIP(System Integrity Protection ) ,它禁止让软件以 root 身份来在 mac 上运行,并且对于目录 ...
- python3yupython2的差别
1.长整型 # python2中才有长整型概念,python3中只有整形一说 # 定义方法:变量名=整数+l (小写L) #python2环境下 >>> a=123456789123 ...
- Android自定义顶部栏及侧滑菜单和fragment+viewpag滑动切换的实现
嘿嘿嘿,关于android滑动的操作,是不是经常都会用到呢. 我肯定也要学习一下啦. https://blog.csdn.net/u013184970/article/details/82882107 ...
- 【FPGA篇章六】FPGA编译向导:详解编译预处理功能
欢迎大家关注我的微信公众账号,支持程序媛写出更多优秀的文章 Verilog HDL语言和C语言一样也提供了编译预处理功能. Verilog HDL允许在程序中使用特殊的编译预处理语句. 在编译时,通常 ...
- NEON中的vshr vshl vext中的位移参数必须为编译时字面常量
NEON中的vshr指令中位移数量参数必须为compile time literal constant,因为该参数是被encoded as part pf ARM instruction itself ...
- Go实战面试备忘录
原文地址:https://blog.likeli.top/posts/面试/go面试备忘录/ 一个小厂的面试,记录一下,答案不对的,请帮忙更正下 go部分 map底层实现 map底层通过哈希表实现 s ...