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 ...
随机推荐
- 字符串的z型转换
class Solution(object): def convert(self, s, numRows): if numRows==1: return ...
- Ubuntu 修改 hosts 文件
sudo vi /etc/hosts sudo /etc/init.d/networking restart
- numpy库的学习笔记
一.ndarray 1.numpy 库处理的最基础数据类型是由同种元素构成的多维数组(ndarray),简称“数组”. 2.ndarray是一个多维数组的对象,ndarray数组一般要求所有元素类型相 ...
- 解决从dockerhub上下载debian:jessie失败
解决从dockerhub上下载debian:jessie失败 笔者使用docker build 构建镜像出现下面的错误 Step 1/12 : FROM debian:jessie Get https ...
- 标准库hashlib模块
hashlib模块用于加密相关的操作,3.x里代替了md5模块和sha模块,主要提供 SHA1, SHA224, SHA256, SHA384, SHA512, MD5 算法(都是基于hash的算法, ...
- Recursion and System Stack
递归是计算机科学中一个非常重要的概念,对于斐波那契那种比较简单的递归,分析起来比较容易,但是由于二叉树涉及指针操作,所以模仿下遍历过程中系统栈的情况. 以二叉树中序遍历为例演示: //二叉树定义 st ...
- 一张图告诉你E-R图怎么画
E-R图也称实体-联系图(Entity Relationship Diagram),提供了表示实体类型.属性和联系的方法,用来描述现实世界的概念模型. 它是描述现实世界关系概念模型的有效方法.是表示概 ...
- POJ1088 滑雪题解+HDU 1078(记忆化搜索DP)
Description Michael喜欢滑雪百这并不奇怪, 因为滑雪的确很刺激.可是为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你.Michael想知道 ...
- 一只简单的网络爬虫(基于linux C/C++)————浅谈并发(IO复用)模型
Linux常用的并发模型 Linux 下设计并发网络程序,有典型的 Apache 模型( Process Per Connection ,简称 PPC ), TPC ( Thread Per Conn ...
- 动态SQL各个标签作用以及注意事项详解
创建com.mybatis包,包含:UserMapper.xml和mybatis-config.xml UserMapper.xml代码: <?xml version="1.0&quo ...