概述
LinkedList本质上是一个基于双向链表实现 的数据结构,因此,LinkedList中保存的各个Item元素也是有序 的,同时也是可以重复 并且可以为null 的。同时因为他是基于双向链表的实现,因此他在存储元素的过程中,不需要像ArrayList那样去动态的扩容。 另外,LinkedList是非线程安全的,因此,在面向多线程并发业务时,需要注意。当然解决方法也是有的,而且很简单,详见 《基于JDK1.8的ArrayList源码分析》一文中"总结"部分。
LinkedList的定义
csharp
public class LinkedList<E>extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
......
}
从源码可以看出,LinkedList实现了List, Cloneable, java.io.Serializable接口,这三个接口的作用在《基于JDK1.8的ArrayList源码分析》一文中已经详细讲解过了,这里不再赘述。另外,他还实现了Deque接口,该接口让LinkedList具备了双向链表的数据结构的能力,并为其提供了基础功能。
但相比于ArrayList,LinkedList因为没有实现RandomAccess接口,因此他不具备数组那样的通过下标直接高效的访问指定位置Item的能力,他只能通过迭代的方式,去访问满足条件的Item元素。
注意:
dart
不是LinkedList不具备像ArrayList那样通过下标访问(get(int position))指定位置Item的能力,
而是说通过LinkedList通过下标访问指定位置的Item的效率非常低,因为他必须通过指针一个一个的
移动到指定位置去,速度很慢,效率很低;而通过Iterator迭代器访问的话,则效率很高。
LinkedList的优缺点:
优点:
因其底层数据结构是链表,所以可想而知,它的增删只需要移动指针即可,故时间效率较高 。不需要批量扩容 ,也不需要预留空间 ,所以空间效率 比ArrayList
高。
缺点:
就是需要随机访问 元素时,时间很慢,效率很低 ,虽然底层在根据下标查询Node的时候,会根据index判断目标Node在前半段还是后半段,然后决定是顺序还是逆序查询 ,以提升时间效率。不过,总体效率依然很低。
LinkedList 双向链表成员变量
LinkedList的每个节点上有三个字段:
kotlin
当前节点的数据字段(data);
指向上一个节点的字段(prev);
和指向下一个节点的字段(next);
而其每个节点的成员变量如下:
kotlin
private static class Node<E> {
E item; // 当前节点
Node<E> next; // 当前节点的下一个节点
Node<E> prev; // 当前节点的上一个节点
Node(Node<E> prev, E element, Node<E> next) {
// 这里的 this 指的是 当前节点对象 Node
this.item = element;
this.next = next;
this.prev = prev;
}
}
因此,每个节点上有三个字段:当前节点的数据字段(item),指向上一个节点的字段(prev),和指向下一个节点的字段(next)。
LinkedList的成员变量
vbscript
public class LinkedList<E>extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
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个成员变量都被transient
关键字修饰,表示LinkedList在被序列化时,他们不参与/不被序列化
。
之所以 LinkedList 要保存链表的第一个节点和最后一个节点是因为,链表数据结构相对于数组数据结构,优点 在于增删
,缺点 在于查找
。
保存了LinkedList 的头尾两端(first/last)节点,当以索引index
来查找Item元素的时候,就可以根据 index
和 size/2
的大小,来决定从头部节点查找还是从尾部节点查找(类似于二分法查找),这也算是一定程度上弥补了链表数据结构不适合直接通过索引index
来查找Item元素的缺点。
LinkedList构造函数
LinkedList无论是无参构造函数还是有参构造函数,都非常简单:
scss
/**
* Constructs an empty list.
*/
public LinkedList() {
}
/**
* Constructs a list containing the elements of the specified collection,
* in the order they are returned by the collection's iterator.
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
从上面源码看,实现非常简单,LinkedList的无参构造函数,实现直接是空的,啥也没做;有参构造函数内部则是先调用LinkedList的无参构造函数,然后再执行addAll(c)
操作,将入参Collection集合c添加到链表里面去;而addAll(c)
内部又调用了 addAll(size, c)
方法将Collection集合c添加到指定的位置size上。
因此,这里我们可以这样理解:如果想让集合在LinkedList尾部插入,就调用addAll(c)
方法;如果是想让集合在指定位置插入,就调用addAll(size, c)
方法。
addAll(c)
方法内部直接调用的是addAll(size, c)
方法,下面我们看看addAll(size, c)
方法的源码实现:
从上面的源码实现逻辑,可以知道LinkedList 批量添加节点实现大体分下面几个步骤:
perl
1、检查索引值index是否在LinkedList长度范围内,不在的话,就抛出下标越界异常
2、保存 当前节点index 位置 和 上一个节点index-1 位置。
3、将入参Collection集合c转化为数组,通过for循环将数组中的元素封装为节点Node添加到LinkedList链表中。
4、更新链表长度;更新modCount表示LinkedList链表发生了结构性的变化;
5、返回 true 表示添加成功。
6、链表批量增加,是靠for循环遍历原数组,依次执行插入操作。ArrayList是通过System.arraycopy()方法完成批量插入的。
7、通过下标index获取某个node节点的时候,会根据index处于前半段还是后半段,进行一个折半,以提升查询效率(类似文章开头提到的二分法查找)
LinkedList的增删查改
LinkedList添加单个节点
LinkedList添加单个节点有三种种场景:
scss
1、执行添加add(E e),不指定添加方式,这时默认是添加在链表的末尾;
2、添加在LinkedList链表的链头addFirst(E e);
3、添加在LinkedList链表的指定的index下标的位置add(int index, E element);
1、执行添加add(E e)
java
// 添加在链表的末尾
public boolean add(E e) {
linkLast(e);
return true;
}
// 添加单个元素在链表的链尾节点位置
void linkLast(E e) {
// 暂存之前的末节点
final Node<E> l = last;
// 以添加的元素e为节点,构建新的末节点newNode,并将新节点prev指针指向之前的末节点last
final Node<E> newNode = new Node<>(l, e, null);
// 更新尾部节点, 将last索引指向末节点last
last = newNode;
// 如果添加之前链表为空,则新节点newNode也作为头节点first
if (l == null) {
first = newNode;
} else { // 否则将之前的未节点first 的next指针指向新节点newNode
l.next = newNode;
}
size++; // 更新LinkedList链表的长度
modCount++; // 更新 modCount,表示LinkedList链表发生了结构性变化
}
2、添加在链头addFirst(E e)
java
// 添加单个元素在链表的头节点位置
public void addFirst(E e) {
linkFirst(e);
}
// 添加在链表的末尾
private void linkFirst(E e) {
// 添加Item元素之前的头节点
final Node<E> f = first;
// 以添加的元素e为节点,构建新的头节点newNode,并将 下一个节点next 指针指向 之前的头节点
final Node<E> newNode = new Node<>(null, e, f);
// first节点指向将新的节点newNode
first = newNode;
// 如果添加之前链表空,则新的节点newNode也就是末节点last
if (f == null) {
last = newNode;
} else { // 否则之前头节点的 prev 指针指向新的节点newNode
f.prev = newNode;
}
size++; // 更新LinkedList链表的长度
modCount++; // 更新 modCount,表示LinkedList链表发生了结构性变化
}
3、添加在指定位置add(int index, E element)
ini
// 添加在指定的下标索引index位置
public void add(int index, E element) {
// 这里检查给定的下标索引index是否是在[0, size]范围内,如果不是,则抛出下标索引index越界异常
checkPositionIndex(index);
// 如果指定的下标索引index等于 size,说明是将新的元素添加在链表的末尾。
if (index == size) {
linkLast(element);// 将新的元素添加在链表的末尾
} else {
linkBefore(element, node(index));
}
}
// 根据index,获取非空节点Node,这个非空节点位于 index 位置
Node<E> node(int index) {
//通过下标获取某个node 的时候,会根据index处于前半段还是后半段 进行一个折半,以提升查询效率。类似二分法查找。
if (index < (size >> 1)) { // 如果 index > size/2 则从size/2到size区间内开始寻找指定角标的节点
Node<E> x = first;
for (int i = 0; i < index; i++) {
x = x.next;
}
return x;
} else { // 否则在 0到size/2的区间内查找
Node<E> x = last;
for (int i = size - 1; i > index; i--) {
x = x.prev;
}
return x;
}
}
// 在指定的 succ 节点 前,插入新的元素节点 e
void linkBefore(E e, Node<E> succ) {
// 由于 succ 一定不为空,所以可以直接获取 prev 节点
final Node<E> pred = succ.prev;
// 新节点 prev 节点为 pred,next 节点为 succ
final Node<E> newNode = new Node<>(pred, e, succ);
// 原节点的 prev 指向新节点
succ.prev = newNode;
// 如果 pred 为空即头节点出插入了一个节点,则将新的节点赋值给 first 索引
if (pred == null) {
first = newNode;
} else { // 否则 pred 的下一个节点改为新节点
pred.next = newNode;
}
size++; // 更新LinkedList链表的长度
modCount++; // 更新 modCount,表示LinkedList链表发生了结构性变化
}
在node(int index)方法中,我们提到根据index,获取非空节点Node,这个非空节点位于 index 位置,这里之所以确定node(int index)方法返回的节点为非null,是因为:
scss
1、如果索引index不在[0,size]范围内,那么在checkPositionIndex(index)方法进行index的合法性检测时就会抛出异常;
2、如果一开始链表中没有元素,表示LinkedList链表为空(Empty,非null),此时size = 0,如果这时向索引 `index = 0` 的位置添加元素,那么if判断:
if (index == size) {
linkLast(element);
} else {
linkBefore(element, node(index));
}
if条件成立,这时直接调用 `linkLast(element);` 方法去执行添加元素的逻辑。不会走到linkBefore()-->Node<E> node(int index)里面去。
LinkedList.addAll()
LinkedList的批量添加方法addAll(Collection<? extends E> c),内部调用的是addAll(int index, Collection<? extends E> c)方法,这跟有参构造时的执行逻辑是一样的。这里就不再赘述。
LinkedList的修改set()
perl
public E set(int index, E element) {
checkElementIndex(index); // 判断下标索引index是否越界
Node<E> x = node(index); // 通过下标索引,调用node(int index)方法获取指定位置的节点信息
E oldVal = x.item; // 暂存指定位置的节点信息
x.item = element; // 用新的节点信息替代下标索引位置的节点
return oldVal; // 返回原下标索引位置的节点信息
}
LinkedList的查询
LinkedList链表的查询分以下几种场景:
scss
1、查询指定索引index处的节点信息:E get(int index);
2、查询LinkedList链表头的节点信息:E getFirst();
3、查询LinkedList链表尾的节点信息:E getLast();
4、查询某个节点信息在LinkedList链表中的位置:indexOf(Object o);
5、查询LinkedList链表中是否包含某个节点信息:boolean contains(Object o);
1、查询指定索引index节点get(int index)
perl
public E get(int index) {
checkElementIndex(index); // 判断下标索引index是否越界
// 通过下标索引,调用node(int index)方法获取指定位置的节点信息,然后返回该节点信息的Item元素
return node(index).item;
}
node()方法前面已经详细分析过了,这里不再赘述,后面也不再提示;
2、查询链表头节点getFirst()
csharp
public E getFirst() {
final Node<E> f = first; // 直接获取LinkedList链表的成员变量first作为链表的头结点
if (f == null) { // 头结点为null,抛出异常
throw new NoSuchElementException();
}
return f.item;
}
3、查询链表尾节点getLast()
csharp
public E getLast() {
final Node<E> l = last; // 直接获取LinkedList链表的成员变量last作为链表的头结点
if (l == null)
throw new NoSuchElementException(); // 尾结点为null,抛出异常
return l.item;
}
4、查询某个节点的位置indexOf()
查询某个节点信息在LinkedList链表中的位置,有两种查询方式,一是从头结开始往尾节点查询;另一种是从尾结点开始向头结点查询;
从头结点开始往尾节点查询的实现:
从尾结点开始向头结点查询的实现:
这两个方法有一个缺陷:
sql
如果LinkedList链表中,有多个null或者Empty节点对象时,该方法只能查找到第一个。查找不到其他的。
5、查询链表是否包含某个节点contains(Object o)
typescript
public boolean contains(Object o) {
return indexOf(o) != -1;
}
该方法实现很简单,直接调用indexOf(o)方法怕 从头结点开始向尾结点,通过 for循环注意查询链表中是否有指定节点的信息。
LinkedList的删除
LinkedList链表的删除,分以下几种场景:
scss
1、删除默认操作:remove();
2、删除链表的第一个节点:removeFirst()/pop();
3、删除指定的元素:remove(Object o)/removeFirstOccurrence(Object o);
4、删除指定的下标索引index对应的节点元素:remove(int index);
5、删除链表的最后一个节点:removeLast();
移除第一个remove()
对于场景1和场景2,其实最后执行的都是removeFirst()方法,下面我们看看remove()方法内部的执行情况:
csharp
public E remove() {
return removeFirst();
}
remove(o)方法内部调用的是removeFirst()方法,我们再看看pop()方法内部的执行情况:
csharp
public E pop() {
return removeFirst();
}
pop()方法内部调用的也是removeFirst()方法。因此我们来看看removeFirst()方法的实现:
ini
public E removeFirst() {
final Node<E> f = first; // 获取链表的第一个节点 first,他是链表的成员变量
if (f == null) {
throw new NoSuchElementException();
}
return unlinkFirst(f); // 调用unlinkFirst()方法执行remove移除操作
}
private E unlinkFirst(Node<E> f) {
final E element = f.item; // 暂存 f 节点的 元素信息
final Node<E> next = f.next; // 暂存 f 节点的 下一个节点的信息
// TODO:下面的 f 清空及后面的if...else...操作完成后,GC将会回收节点内存。
f.item = null; // 清空 f 节点的的元素信息
f.next = null; // // 清空 f 节点的的下一个节点的信息
first = next; // 重新将 原本的next 下一个节点的信息赋值给first,使其成为新的头结点
if (next == null) { // 如果 next 节点信息为空,表示后面first节点后面没有其他的节点了,清空 last 节点
last = null;
} else { // next 节点信息不为空,那么就将原本的next节点的上一个节点清空(s上一个节点就是被删除的那个节点)
next.prev = null;
}
size--; // 更新链表的长度
modCount++; // 更新modCount,表示链表发生了结构性改变。
return element;
}
unlinkFirst()方法主要干了以下几件事情:
1、暂存要移除的节点元素item及要移除的节点的下一个节点next;
2、清空要移除的节点的元素信息item、下个节点的信息next,将要移除的节点的下一个节点next赋值给链表的第一个节点first;
3、清空next节点及对应的上一个节点;
4、更新 size和modCount;
删除指定元素remove(Object o)
删除指定元素有两个实现方法,分别是removeFirstOccurrence(Object o)方法和remove(Object o)方法,但其实removeFirstOccurrence(Object o)方法内部调用的就是remove(Object o)方法:
typescript
public boolean removeFirstOccurrence(Object o) {
return remove(o);
}
因此我们看看remove(Object o)方法的内部实现:
删除链表所有元素clear()
转化为数组toArray()
其内部就是通过for循环将LinkedList的所有节点都保存到一个长度为size的Object数组中:
ini
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node<E> x = first; x != null; x = x.next) {
result[i++] = x.item;
}
return result;
}
总结:
1、LinkedList 是双向列表的数据结构实现。因此,LinkedList中保存的各个Item元素是有序 的,可以重复 的,并且可以为null的。同时因为他是基于双向链表的实现,因此他在存储元素的过程中,不需要像ArrayList那样去动态的扩容。
2、链表批量增加,是通过for循环遍历原Collection数组,依次执行插入节点操作,增加一定会修改modCount,表示LinkedList链表发生了结构性改变。而ArrayList则是通过调用Native层的System.arraycopy()方法完成批量插入的。
3、通过下标index获取某个指定位置的node的时候,会采取类似二分法查找的方式,根据index处于前半段还是后半段进行查询,以提升查询效率
。
4、通过下标index删除指定节点,会先根据index索引在node()方法里面通过for循环遍历LinkedList链表,找到对应的Node节点信息,然后去链表上unlink掉这个Node。
5、通过元素Object o删除,会先去通过for循环遍历LinkedList链表,确认链表中是否包含该Node节点,如果有,去链表上unlink掉这个Node。
6、但凡对LinkedList链表执行增加、插入、删除操作,就一定会更新modCount参数,表示LinkedList链表发生了结构性变化。
7、修改set()操作,也是先通过node()方法,根据下标index索引通过for循环找到Node,然后替换值。
8、查找get()操作,相比于修改set()操作,少了然后替换值的操作,其他操作一样。
9、修改set()操作和查找get()操作,都不会更新modCount。