文章目录:

1.看看关于LinkedList源码开头的注释

2.LinkedList中的属性

3.LinkedList中的方法

3.1 push、offer方法

3.2 添加元素的一系列add方法

3.3 linkFirst方法

3.4 linkLast方法

3.5 linkBefore方法

3.6 移除元素的一系列remove方法

3.7 unlinkFirst方法

3.8 unlinkLast方法

3.9 unlink方法

3.10 获取元素的get、getFirst、getLast方法

3.11 size方法

3.12 peek、peekFirst、peekLast方法

3.13 pop、poll、pollFirst、pollLast方法

3.14 set方法

3.15 contains方法

3.16 clear方法


1.看看关于LinkedList源码开头的注释

* Doubly-linked list implementation of the {@code List} and {@code Deque}
* interfaces.  Implements all optional list operations, and permits all
* elements (including {@code null}).
*
* <p>All of the operations perform as could be expected for a doubly-linked
* list.  Operations that index into the list will traverse the list from
* the beginning or the end, whichever is closer to the specified index.
*
* <p><strong>Note that this implementation is not synchronized.</strong>
* If multiple threads access a linked list concurrently, and at least
* one of the threads modifies the list structurally, it <i>must</i> be
* synchronized externally.  (A structural modification is any operation
* that adds or deletes one or more elements; merely setting the value of
* an element is not a structural modification.)  This is typically
* accomplished by synchronizing on some object that naturally
* encapsulates the list.

从这段注释中,我们可以得知 LinkedList 是通过一个双向链表来实现的,它允许插入所有元素,包括 null,同时,它是线程不同步的。

  • LinkedList集合底层结构是带头尾指针的双向链表。
  • LinkedList是非线程安全的。
  • LinkedList集合中存储元素的特点:有序可重复,元素带有下标,从0开始,以1递增。
  • LinkedList集合的优点:在指定位置插入/删除元素的效率较高;缺点:查找元素的效率不如ArrayList。

如果对双向链表这个 数据结构很熟悉的话,学习 LinkedList 就没什么难度了。下面是双向链表的结构:

双向链表每个结点除了数据域之外,还有一个前指针和后指针,分别指向前驱结点和后继结点(如果有前驱/后继的话)。另外,双向链表还有一个 first 指针,指向头节点;和 last 指针,指向尾节点。


2.LinkedList中的属性

//双向链表中结点个数
transient int size = 0;//指向头结点的指针
transient Node<E> first;//指向尾结点的指针
transient Node<E> last;

关于LinkedList中的Node节点结构,它其实是在 LinkedList 里定义的一个静态内部类,它表示链表每个节点的结构,包括一个数据域 item,一个后置指针 next,一个前置指针 prev。

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

3.LinkedList中的方法

3.1 push、offer方法

push、offer方法内部都是调用的相应的add方法,所以直接看下面的add方法源码解析。

public void push(E e) {addFirst(e);
}public boolean offer(E e) {return add(e);
}public boolean offerFirst(E e) {addFirst(e);return true;
}public boolean offerLast(E e) {addLast(e);return true;
}

3.2 添加元素的一系列add方法

在LinkedList集合源码中,添加元素很多时候都会用到 add、addFirst、addLast 这几个,而查看源码发现,这几个方法的内部实际上都调用了 linkFirst、linkLast 、linkBefore 这三个方法。

public boolean add(E e) {linkLast(e);return true;
}public void addFirst(E e) {linkFirst(e);
}public void addLast(E e) {linkLast(e);
}

所以下面着重来说一下 linkFirst、linkLast 、linkBefore 这三个方法。

3.3 linkFirst方法

对于链表这种数据结构来说,添加元素的操作无非就是在表头/表尾插入元素,又或者是在指定位置插入元素。因为 LinkedList 有头指针和尾指针,所以在表头或表尾进行插入元素只需要 O(1) 的时间,而在指定位置插入元素则需要先遍历一下链表,所以复杂度为 O(n)。

而linkFirst表面上翻译就是 链表首位,也就是在表头添加元素,其源码及添加元素分析过程如下:

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;elsef.prev = newNode;size++;modCount++;
}

当我们向双向链表的表头插入一个结点 e 时,这个结点的前驱指针肯定是 null,那么在插入之后,这个 e 结点的后继指针是什么呢?(在e插入之前,原先的表头结点就是表头结点,在e插入到原表头的前面成为了新的表头之后,此时原表头结点不就是当前e结点的后继结点了吗?所以e结点的后继指针自然也就指向了原表头结点,所以这里e的后继指针就是最初链表的头指针first),所以要在插入之前先获取到头指针 f = first,然后将头指针对应的结点修改为新插入的结点e(newNode),对应源码的前三行。

而这个if判断的是:如果在插入之前链表的头指针为空(换句话说,当前链表中没有元素,e结点插入之后只有这一个元素),那么此时e结点肯定既是头结点、也是尾结点啊,所以这里将 last 尾结点修改为刚刚插入的 e 结点。

下面的else是说:当前链表中有多个元素了话,当你在某个结点x之前新插入一个结点e,那么结点x的后继部分肯定没有变化,变化的是它的前驱部分,前驱指针所指内容就变成了 新插入的结点e,即 f.prev = newNode。

3.4 linkLast方法

这个方法和 linkFirst 差不多的,无非是一个在表头插入元素,一个在表尾插入元素。

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;elsel.next = newNode;size++;modCount++;
}

首先插入之前,先获取一下原表尾结点的内容(l = last),然后插入新的结点e,这个结点e的后继指针肯定是 null(因为此时它成了链表的表尾结点),而它的前驱指针指向的就应该是上一个表尾结点 l,所以新表尾结点e的内容就是(l,e,null)。然后更新双向链表的尾指针 last 为新插入的结点newNode。

下面的if判断的是:如果在插入之前获取的尾指针 l 为空(也就是说当前链表中没有元素),那么新插入一个元素之后,这个元素肯定既是头结点、也是尾结点,所以这里将它的头指针也更新为 newNode。

else说的是:当插入之前链表中有多个元素,那么在表尾新插入一个元素之后,还要最终修改一下原表尾结点的后继指针,让它指向新的表尾结点。

3.5 linkBefore方法

上面两个方法分别都是在表头、表尾插入元素,那么现在该说一下在中间某个随机位置插入元素的方法了,源码如下:

void linkBefore(E e, Node<E> succ) {// assert succ != null;final Node<E> pred = succ.prev;final Node<E> newNode = new Node<>(pred, e, succ);succ.prev = newNode;if (pred == null)first = newNode;elsepred.next = newNode;size++;modCount++;
}

对应源码来说,这里要插入的结点就是 e,e要插入到 succ 结点的前面,同时位于 pred 结点的后面,而在插入之前(succ的前驱就是pred),所以先获取到这个前驱结点 pred,然后修改e结点的内容(pred,e,succ),那么此时 succ 的前驱就变成了 e,所以更新 succ 的前驱指针指向 newNode。

if判断的是:如果插入之前succ结点的前驱指针pred为空,也就是说此时succ是表头结点,那么e插入之后,它就成了新的表头结点,所以这里让first头指针指向newNode。

else则是说插入之前链表中有多个元素,那么在succ指定结点的前面插入新的结点之后,还要修改succ原前驱节点pred的后继指针,使其指向刚插入的e结点newNode。

3.6 移除元素的一系列remove方法

在LinkedList集合源码中,移除元素很多时候都会用到 remove、removeFirst、removeLast 这几个,而查看源码发现,这几个方法的内部实际上都调用了 unlinkFirst、unlinkLast 、unlink 这三个方法。unlink方法中的node方法是对链表进行遍历的。

public E remove() {return removeFirst();
}public E remove(int index) {checkElementIndex(index);return unlink(node(index));
}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;
}public E removeFirst() {final Node<E> f = first;if (f == null)throw new NoSuchElementException();return unlinkFirst(f);
}public E removeLast() {final Node<E> l = last;if (l == null)throw new NoSuchElementException();return unlinkLast(l);
}

所以下面着重来说一下 unlinkFirst、unlinkLast 、unlink 这三个方法。

3.7 unlinkFirst方法

删除操作与添加操作大同小异,需要把当前节点的前驱节点的后继修改为当前节点的后继,以及当前节点的后继结点的前驱修改为当前节点的前驱。

unlinkFirst方法是在表头进行元素的删除,首先做的是将要删除元素的item值保存到一个临时变量element中,最终返回。同时将要删除元素的后继指针保存到next临时指针中。然后将元素删除(即 f.item=null,f.next=null,因为删除元素位于表头,所以 f.prev 本身就是 null),删除之后链表中的第二个元素就成为了新的表头,所以修改 first 头指针使其指向之前保存的 next 临时指针(头指针指向后一个结点)。

if判断的是:如果next为空,意思是说所删除元素的后继指针如果为空(又因为该方法是在表头进行元素删除),所以此时链表中仅存的这个结点被删除了,那么整个链表就清空了,所以尾指针 last 就为空了。

else说的是:删除之前链表中存在多个元素,那么当头结点被删除之后,原先头结点之后的那个结点就成了新的头结点,所以这个新的头结点的头指针肯定是null,所以 next.prev=null。

最终返回的是被删除元素的item值。

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 GCfirst = next;if (next == null)         last = null;elsenext.prev = null;size--;modCount++;return element;
}

3.8 unlinkLast方法

这个方法和上面的差不多,上面的是在表头进行元素的删除,这个是在表尾进行元素的删除。

同样是先保存这个表尾结点的 item 值、prev前驱指针(因为后继指针本身为 null 无需保存),之后将 item、prev 置为 null。当前表尾结点被删除之后,它前面的那个结点就成了新的表尾结点,所以需要将链表的 last 尾指针指向原表尾结点的前驱结点(last=prev)。

if判断的是:如果要删除的表尾结点的前驱为null,则说明此时链表中只有这一个结点,删除之后,链表清空,所以将链表的头指针修改为 null。

else说的是:删除之前链表中有多个元素,将当前表尾结点删除之后,那么它前面的那个结点成了新的表尾结点,所以这个新的表尾结点的后继指针肯定为 null,即 prev.next=null。

最终返回的是被删除元素的item值。

private E unlinkLast(Node<E> l) {// assert l == last && l != null;final E element = l.item;final Node<E> prev = l.prev;l.item = null;l.prev = null; // help GClast = prev;if (prev == null)first = null;elseprev.next = null;size--;modCount++;return element;
}

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

首先还是先获取要删除元素的item值、next后继指针、prev前驱指针,保存到三个局部变量中。

第一个 if/else:如果前驱指针为null,那么意味着删除的是表头结点,删除之后,新的表头结点为原表头结点的next后继结点,所以将链表的头指针修改指向原表头结点的next后继指针。如果前驱指针不为 null,意味着在链表中间某个位置进行删除操作,需要先修改一下被删除结点的前驱节点的后继指针,使其指向被删除结点的后继指针;之后避免指针冲突,将被删除结点的前驱指针置为null(切断被删除结点的前驱指针这条线)。

第二个 if/else:如果后继指针为null,那么意味着删除的是表尾结点,删除之后,新的表尾结点为原表尾结点的prev前驱结点,所以将链表的尾指针修改指向原表尾结点的prev前驱指针。如果后继指针不为 null,意味着在链表中间某个位置进行删除操作,第一个if/else已经切断了被删除结点的前驱线路,这里还需要修改一下被删除结点的后继节点的前驱指针,使其指向被删除结点的前驱指针;之后避免指针冲突,将被删除结点的后继指针置为null(切断被删除结点的后继指针这条线)。最后将被删除结点的item值也置为null。

最终返回的是被删除元素的item值。

3.10 获取元素的get、getFirst、getLast方法

在LinkedList集合源码中,获取元素很多时候都会用到 get、getFirst、getLast 这几个。

public E get(int index) {checkElementIndex(index);return node(index).item;
}public E getFirst() {final Node<E> f = first;if (f == null)throw new NoSuchElementException();return f.item;
}public E getLast() {final Node<E> l = last;if (l == null)throw new NoSuchElementException();return l.item;
}

getFirst方法:获取双向链表中的第一个元素。首先就是拿到当前链表的头指针所指向的头结点,如果为空,则抛出没有这个元素异常;不为空直接就返回表头结点的item值。

getLast方法:获取双向链表中的最后一个元素。首先就是拿到当前链表的尾指针所指向的尾结点,如果为空,则抛出没有这个元素异常;不为空直接就返回表尾结点的item值。

而在get方法的源码中,第一行所做的是进行集合下标是否越界的判断,这里不再多说了。主要是它第二行调用了一个node方法,源码如下:

Java集合源码剖析——基于JDK1.8中LinkedList的实现原理相关推荐

  1. 【Java集合源码剖析】Hashtable源码剖析

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/36191279 Hashtable简介 Hashtable同样是基于哈希表实现的,同样每个元 ...

  2. 【Java集合源码剖析】HashMap源码剖析

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/36034955 您好,我正在参加CSDN博文大赛,如果您喜欢我的文章,希望您能帮我投一票,谢 ...

  3. 【Java集合源码剖析】TreeMap源码剖析

    2019独角兽企业重金招聘Python工程师标准>>> 前言 本文不打算延续前几篇的风格(对所有的源码加入注释),因为要理解透TreeMap的所有源码,对博主来说,确实需要耗费大量的 ...

  4. 转:【Java集合源码剖析】LinkedHashmap源码剖析

    转载请注明出处:http://blog.csdn.net/ns_code/article/details/37867985   前言:有网友建议分析下LinkedHashMap的源码,于是花了一晚上时 ...

  5. 【Java集合源码剖析】Java集合框架

    2019独角兽企业重金招聘Python工程师标准>>> Java集合工具包位于Java.util包下,包含了很多常用的数据结构,如数组.链表.栈.队列.集合.哈希表等.学习Java集 ...

  6. Java集合源码分析(二)ArrayList

    ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...

  7. ConcurrentHashMap源码解析——基于JDK1.8

    ConcurrentHashMap源码解析--基于JDK1.8 前言 这篇博客不知道写了多久,总之就是很久,头都炸了.最开始阅读源码时确实是一脸茫然,找不到下手的地方,真是太难了.下面的都是我自己阅读 ...

  8. Java集合源码浅析(一) : ArrayList

    (尊重劳动成果,转载请注明出处:https://yangwenqiang.blog.csdn.net/article/details/105418475冷血之心的博客) 背景 一直都有这么一个打算,那 ...

  9. 【本人秃顶程序员】深入理解Java——ConcurrentHashMap源码的分析(JDK1.8)

    ←←←←←←←←←←←← 快!点关注 一.前提 在阅读这篇博客之前,希望你对HashMap已经是有所理解的,如果你对java的cas操作也是有一定了解的,因为在这个类中大量使用到了cas相关的操作来保 ...

  10. 吐血整理:Java线程池源码分析(基于JDK1.8建议收藏)

    文章目录 一.引言 二.线程池的参数介绍 1.ThreadPoolExecutor的UML图 三.线程池的使用 1.线程池的工作原理 2.线程池类型 2.1.newCachedThreadPool使用 ...

最新文章

  1. FPGA转载文章目录
  2. android studio 如何提示方法的用法
  3. ibatis如何支持clob 和blob
  4. JavaScript事件绑定的方法说明 收藏
  5. 欧拉公式cos_对复数,复数平面以及欧拉公式的理解
  6. Phinecos(洞庭散人) 专注于开源技术的研究与应用 TinyXML:一个优秀的C++ XML解析器
  7. Linux下的LD_PRELOAD环境变量与库打桩
  8. R包ggseqlogo |绘制序列分析图
  9. 第七章:nginx的rewrite规则详解
  10. 双层感知机解决异或问题
  11. fastboot命令大全
  12. NRF24L01-状态寄存器
  13. 「PyTorch自然语言处理系列」7. 自然语言处理的进阶序列模型
  14. C语言的美学:斐波那契,你会吗?
  15. 解决word插入的图片不能正常显示
  16. dij最短路+堆优化
  17. 小程序楼层索引,将汉字转换为拼音并以首字母排序
  18. oracle both from,Oracle trim函数的使用
  19. 通过adb录制视频并通过FFMPEG将MP4转换成GIF格式(二)
  20. 为什么没有下划线_资料1907:xumin字体打不出下划线?凌哥英语送您改进版!

热门文章

  1. 专题:手把手学习硬件基础之手册阅读------TM1620
  2. 关于二进制补码+CS5463
  3. 微信小程序实现tab切换
  4. MFC控件-Spin控件和编辑框控件Edit Control绑定使用
  5. Spark 学习: spark 原理简述与 shuffle 过程介绍
  6. 一种去除U盘写保护的可行方法(dd 命令解决)
  7. ubuntu18下成功安装Remastersys备份当前系统成ISO文件
  8. 腾讯云文档数据库MongoDB怎么样?腾讯云文档数据库MongoDB有什么优点?
  9. 网络共享计算机设置方法,如何设置网络共享(局域网共享如何设置)
  10. 步进驱动系统:步进电机与步进驱动器控制原理简述