LinkedList实现原理(JDK1.8)

LinkedList底层采用双向链表,如果对链表这种结构比较熟悉的话,那LinkedList的实现原理看明白就相当容易。

链表通过“指针”将一组零散的内存块串联起来使用,每一个元素(节点)通过指针指向它的下一个元素,最后一个节点的下一个指向为null,而双向链表就是除头节点的每一个元素都有指针同时再指向它的上一个元素。链表不同于数组,由于其地址的不连续,且元素占用大小的不确定,所以没法根据地址直接取值,获取元素时需要遍历访问,而双向链表相比于单向链表,一定程度上占用了额外的内存,但支持了双向遍历,加快了元素索取。

public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable

LinkedList继承于AbstractSequentialList抽象类,AbstractSequentialList又继承于AbstractList,最终实现了List的接口,所以LinkedList支持List集合的一些常规操作,但同时LinkedList又实现了Queue接口,所以LinkedList也支持队列的一些使用,例如peek、pop等。

1.成员变量

    transient int size = 0;

    /**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first; /**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;

LinkedList的主要成员变量就3个,大小size、头结点first、尾节点last,这也符合双向链表的特点,根据头或者尾部节点就可以遍历整个链表。至于节点类型,则是内部类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类就简简单单三个属性,也即双向链表节点必须的3个要素,当前节点数据item,下一节点next和上一节点prev。

2.构造方法

    public LinkedList() {
} public LinkedList(Collection<? extends E> c) {
this();
// 添加所有元素
addAll(c);
}

LinkedList主要就两个构造方法,一个无参的构造,什么也没做,另一个接受集合的构造,该方法初始化了LinkedList对象并添加了所有集合c的元素。

至于addAll方法,我们后面再看。

LinkedList因为基于链表,所以不需要提前申请内存大小,也就不存在初始化指定容量大小,因而LinkedList是没有初始化指定size的构造方法的。

3.添加元素

3.1.尾部添加

LinkedList的添加方法有很多,首先最简单也是最常用的尾部添加。

    public boolean add(E e) {
linkLast(e);
return true;
}

主要逻辑就是linkLast(E e) 了。

    void linkLast(E e) {
final Node<E> l = last;
// 构造新的节点,上一节点指向原来的last
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
// 操作数自增
modCount++;
}

尾部添加,就是构造新的节点newNode,并将改节点的上一节点prev指向原来的last,同时改节点为新的last节点。l == null判断是否当前是第一次添加,如果 l 为 null,则newNode同时也是头结点,当前集合中仅有newNode一个元素,不为null时,因为双向链表,所有 l 的下一个节点next指向了newNode。

最后就是大小size自增与操作计数modCount的自增,尾部添加元素就完成了。

尾部操作除了add,还有个addLast(E e) 方法,两者除了返回值不一样,没有任何差别了,都是调用的linkLast方法。

    public void addLast(E e) {
linkLast(e);
}
3.2.中间添加
    public void add(int index, E element) {
// 范围检查
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}

对于中间添加,需要首先进行范围检查,即保证插入位置index在[0, size]之间,否则抛出数组越界异常IndexOutOfBoundsException,呃……数组越界……

    private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
} private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}

如果index == size,其实就是尾部插入,所以调用了linkLast,这个刚刚尾部插入已经说过。

如果index < size,中间插入的时候,需要分两步:

  1. node(int index)方法获取到index位置的元素succ
  2. linkBefore(E e, Node succ)将需要插入的元素element连接到succ后面

node方法是一个频繁被调用的方法,LinkedList 的很多操作都依赖于该方法查找到对应的元素。根据索引 index 获取元素时,因为双向链表的支持前后遍历,所以进行了位置判断,index < (size >> 1),与中间位置比较,靠前则前序遍历,否则后序遍历。

    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上一个节点(后序则是下一个)位置,获取next(后序使用prev)返回index位置对应的节点Node对象succ。

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

linkBefore和linkLast几乎完全一样,除了一个是添加到 last 节点后,一个是添加到 succ 节点后。

对于中间插入,如果index为0时,其实就是头部插入,这个时候比不用调用node方法去查找元素了,所以LinkedList也提供了一个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++;
}
3.3.批量插入

LinkedList提供尾部批量插入和中间批量插入,但内部实现其实都是调用的addAll(int index, Collection<? extends E> c)。

    public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
} public boolean addAll(int index, Collection<? extends E> c) {
// 范围校验
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
// succ是index位置元素,pred是index的前一个元素
Node<E> pred, succ;
if (index == size) { // 尾部插入
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
// 循环插入
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
// 衔接处理
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred; size += numNew;
modCount++;
return true;
}

addAll(int index, Collection<? extends E> c)方法初一看,好像有些复杂,但明白其原理后,就变得清晰多了。链表插入就如同接水管,先从某一个位置断开水管,然后用接口连接上需要接入的部分。这个方法里,关键的是两个Node对象pred, 和 succ,succ 是index位置元素,pred是index的前一个元素(变动)。

特殊情况 index == size 时,即尾部插入,所以succ 就是null了,而 pred则为尾部节点last。

然后就是循环赋值了,在循环中构造node节点,类似于linkLast。

最后的是衔接处理,如果尾部插入的话,那pred就是尾部节点了(循环赋值时有pred = newNode处理),所以只需要指定last = pred。而中间插入,指明 pred.next = succ、succ.prev = pred即将index位置与新的前一个元素绑定到一起。

4.查找元素

LinkedList 除了提供通用的 get,因为其属性中含有 first 和 last 节点,也提供了 getFirst 和 getLast 方法。

    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 和 getLast,因为是成员变量,省去了查找的过程,直接返回其节点 item 即可。

    public E get(int index) {
// 范围校验
checkElementIndex(index);
// node方法获取节点
return node(index).item;
}

而通过指针的获取,主要就是调用node方法找对index对应的节点,node方法前面已经讲过,不再累述。

5.修改元素

对于LinkedList集合中元素的修改,需要先查找到该元素,然后更改其Node节点数据item即可。

    public E set(int index, E element) {
// 范围检查
checkElementIndex(index);
// 获取index对应位置的Node对象
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}

6.删除元素

LinkedList提供了很多种删除元素的方法,但是内部实现逻辑基本都相同,即找到对应的Node节点,然后将指向该节点的指向替换。

6.1.根据索引移除

我们先来看看根据索引的remove(int index)方法。

    public E remove(int index) {
// 范围检查
checkElementIndex(index);
// 解除节点指针连接
return unlink(node(index));
}

删除时的范围检查就不说了,node方法也不再多提,删除的主要逻辑就是unlink(Node x)方法。

    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;
// 前一节点prev存在则将prev的下一节点指向next,不存在则当前移除节点其实就是头结点,next就是新的first
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
// 下一节点next存在,则将next上一节点指向prev,不存在则说明当前移除的是未节点
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
// 触发GC工作
x.item = null;
size--;
// 操作计数器自增
modCount++;
return element;
}

整个unlink方法就是个标准的双向链表删除操作,三个节点prev,x,next,删除x 其实就是将 prev指向next,并next指向prev,只是其中多了一些特殊的判断。

看了按索引删除的remove,再来看另外两个特例removeFirst 和 removeLast。

    public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
} 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;
}

unlinkFirst就是个简化版的unlink方法,因为只用处理头结点,下一个节点next存在就将next作为新的first。

同理removeLast也是类似,这里各位看官自行进行体会。

    public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
} 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 GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}

而LinkedList还有个无参的remove,这个是Deque接口定义的,实现也就是调用的removeFirst。

    public E remove() {
return removeFirst();
}
6.2.根据元素移除

根据元素移除其实和根据索引移除没有太大差别,只不过找到对应节点的方式发生了变化。

    public boolean remove(Object o) {
// 判断元素是否为null,因为LinkedList支持添加null
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方法。

因为支持双向遍历的特性,LinkedList很人性的提供了前序删除和后序删除的方法,即removeFirstOccurrence与removeLastOccurrence。

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

这两个方法没什么玄幻的,removeLastOccurrence只是反序遍历了集合。

6.3.批量删除

在LinkedList类中,并没有removeAll方法,因为他未对其进行重写,而是使用了父类AbstractCollection的

    public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
// 使用迭代器
Iterator<?> it = iterator();
while (it.hasNext()) {
if (c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}

removeAll的实现原理其实就是迭代删除,迭代器的获取方法iterator()在AbstractCollection类中只是个抽象方法,AbstractList类有其实现,但AbstractSequentialList类中覆写该方法。

    public Iterator<E> iterator() {
return listIterator();
}

iterator方法会调用listIterator(),这个方法实现在AbstractList类中,他调用了listIterator(int index)方法,但LinkedList重写了该方法,所以兜兜转转最终还是回到了LinkedList中。

    public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}

这里ListItr对象是LinkedList的内部类。

    private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;
private Node<E> next;
private int nextIndex;
// 期待计数器
private int expectedModCount = modCount;

ListItr在初始化的时候,会将操作计数器modCount赋值给expectedModCount,而之后的每次remove方法,都会校验expectedModCount与modCount是否相等,否则会抛出异常。

ListItr的remove方法,每次调用后,都将expectedModCount自增,已达到和unlink中modCount++的同步,从而使得modCount == expectedModCount 一直成立,这也是为什么我们循环删除LinkedList元素时需要使用其迭代器的remove方法。

        public void remove() {
// 校验modCount
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException(); Node<E> lastNext = lastReturned.next;
// unlink删除节点逻辑,该方法中有modCount++;
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
// expectedModCount自增
expectedModCount++;
} final void checkForComodification() {
// expectedModCount与modCount必须相等
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}

LinkedList实现原理(JDK1.8)的更多相关文章

  1. HashMap实现原理(jdk1.7),源码分析

    HashMap实现原理(jdk1.7),源码分析 ​ HashMap是一个用来存储Key-Value键值对的集合,每一个键值对都是一个Entry对象,这些Entry被以某种方式分散在一个数组中,这个数 ...

  2. Java中HashMap底层实现原理(JDK1.8)源码分析

    这几天学习了HashMap的底层实现,但是发现好几个版本的,代码不一,而且看了Android包的HashMap和JDK中的HashMap的也不是一样,原来他们没有指定JDK版本,很多文章都是旧版本JD ...

  3. 集合之LinkedList(含JDK1.8源码分析)

    一.前言 LinkedList是基于链表实现的,所以先讲解一下什么是链表.链表原先是C/C++的概念,是一种线性的存储结构,意思是将要存储的数据存在一个存储单元里面,这个存储单元里面除了存放有待存储的 ...

  4. HashMap底层实现原理(JDK1.8)源码分析

    ref:https://blog.csdn.net/tuke_tuke/article/details/51588156 http://www.cnblogs.com/xiaolovewei/p/79 ...

  5. LinkedList实现原理

    原文链接:https://www.jianshu.com/p/56c77c517e71 本文对LinkedList的实现讨论都基于JDK8版本 Java中的LinkedList类实现了List接口和D ...

  6. LinkedList集合(JDK1.8)

    简述 按照上篇笔记ArrayList集合继续进行介绍list的另一个常见子类LinkedList ?LinkedList介绍 1.数据结构 说明:linkedlist的底层数据结构是个双向链表结构,也 ...

  7. 高并发编程系列:ConcurrentHashMap的实现原理(JDK1.7和JDK1.8)

    HashMap.CurrentHashMap 的实现原理基本都是BAT面试必考内容,阿里P8架构师谈:深入探讨HashMap的底层结构.原理.扩容机制深入谈过hashmap的实现原理以及在JDK 1. ...

  8. 6.Java集合-LinkedList实现原理及源码分析

    Java中LinkedList的部分源码(本文针对1.7的源码) LinkedList的基本结构 jdk1.7之后,node节点取代了 entry ,带来的变化是,将1.6中的环形结构优化为了直线型链 ...

  9. Java面试必问之Hashmap底层实现原理(JDK1.8)

    1. 前言 上一篇从源码方面了解了JDK1.7中Hashmap的实现原理,可以看到其源码相对还是比较简单的.本篇笔者和大家一起学习下JDK1.8下Hashmap的实现.JDK1.8中对Hashmap做 ...

随机推荐

  1. SpringBoot与MybatisPlus3.X整合之动态表名 SQL 解析器(七)

    pom.xml <dependencies> <dependency> <groupId>org.springframework.boot</groupId& ...

  2. 学 Python (Learn Python The Hard Way)

    学 Python (Learn Python The Hard Way) Contents: 译者前言 前言:笨办法更简单 习题 0: 准备工作 习题 1: 第一个程序 习题 2: 注释和井号 习题 ...

  3. pyEcharts安装及使用指南

    pyEcharts安装及使用指南 ECharts是一个纯Javascript的图表库,可以流畅的运行在PC和移动设备上,兼容当前绝大部分浏览器,底层依赖轻量级的Canvas类库ZRender,提供直观 ...

  4. java map遍历方式及效率

    本文转载自Java Map遍历方式的选择. 只给出遍历方式及结论.测试数据可以去原文看. 如果你使用HashMap 同时遍历key和value时,keySet与entrySet方法的性能差异取决于ke ...

  5. 别翻了,这篇文章绝对让你深刻理解java类的加载以及ClassLoader源码分析【JVM篇二】

    目录 1.什么是类的加载(类初始化) 2.类的生命周期 3.接口的加载过程 4.解开开篇的面试题 5.理解首次主动使用 6.类加载器 7.关于命名空间 8.JVM类加载机制 9.双亲委派模型 10.C ...

  6. 【微信小程序】踩坑指南(持续更新)

    前言 说明: 基于mpvue框架:mpvue官方文档 语法同vue框架:vue官方文档 小程序中会有一些坑点,这里会就工作中遇到的坑一一列举出来 无说明时请直接看代码注释 v-show无法使用在小程序 ...

  7. 通过C/C++,实现一元一次方程求解

    通过C/C++,实现一元一次方程求解: #include <bits/stdc++.h> using namespace std; string str, str_l, str_r; st ...

  8. nyoj 111-分数加减法 (gcd, switch, 模拟,数学)

    111-分数加减法 内存限制:64MB 时间限制:1000ms 特判: No 通过数:20 提交数:54 难度:2 题目描述: 编写一个C程序,实现两个分数的加减法 输入描述: 输入包含多行数据 每行 ...

  9. python:调用bash

    利用os模块 python调用Shell脚本,有三种方法: os.system(cmd)返回值是脚本的退出状态码 os.popen(cmd)返回值是脚本执行过程中的输出内容 commands.gets ...

  10. 你必须知道的容器日志 (1) Docker logs & logging driver

    本篇已加入<.NET Core on K8S学习实践系列文章索引>,可以点击查看更多容器化技术相关系列文章.监控和日志历来都是系统稳定运行和问题排查的关键,在微服务架构中,数量众多的容器以 ...