集合框架之LinkedList源码分析

时间:2022-12-19 13:56:09


集合框架之​​LinkedList​​源码分析

一、简述

​LinkedList​​​是Java集合中一个全能的结构,底层采用了​​双向链表​​​结构。和​​ArrayList​​也支持空值和重复值,但是使用链表实现,在插入元素效率高,但是遍历查询效率比较低。

​LinkedList​​​是非线程安全的集合类,并发环境,多线程操作​​LinkedList​​,会引发不可预知的错误。

优点:底层是链表,所以增删只需要移动指针,效率很高,不需要批量扩容和预留空间。

缺点:随机访问需要从前到后(从后到前)遍历,随着节点增加,效率一直降低。

可以使用​​List list=Collections.synchronizedList(new LinkedList(...));​​​将​​LinkedList​​变成线程安全的。

二、继承结构

集合框架之LinkedList源码分析

从​​UML​​​图中我们可以很明显看到​​LinkedList​​​实现了​​Deque​​​接口这意味着我们可以把​​LinkedList​​当做队列来使用(队列的一种实现方式)。

基本的结构图:

集合框架之LinkedList源码分析

三、成员变量和节点

成员变量:

transient int size = 0; // 大小size
// 这里的fist指针需要整个过程中满足,(first == null && last == null) || (first.prev == null && first.item != null)
transient Node<E> first; // 指向第一个节点的指针。
// 这里的last指针需要整个过程中满足,(first == null && last == null) || (last.next == null && last.item != null)
transient Node<E> last; // 指向最后一个节点的指针

节点:

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

四、构造函数

无参构造:空的​​List​

public LinkedList() {}

有参构造:构造一个包含指定集合的元素的列表,其顺序由集合的迭代器返回。

public LinkedList(Collection<? extends E> c) {
this();
// 把集合中所有节点添加到list中, 源码详解请看下面的解释
addAll(c);
}

接着我们看一下​​addAll​​方法:看5.1.4

五、核心方法

5.1. 增加相关方法

5.1.1. ​​add(E e)​​:在链表尾部添加元素有两种情况:
  • 链表中没有元素,​​add​​添加元素
  • 链表中有元素,​​add​​添加元素
/**
* 增加一个指定的到链表的末尾
*
* <p>This method is equivalent to {@link #addLast}.
*
* @param e 被插入到链表元素
* @return {@code true} (as specified by {@link Collection#add})
*/
public boolean add(E e) {
linkLast(e);
return true;
}

/**
* Links e as last element.
*/
void linkLast(E e) {
// 保存尾指针
final Node<E> l = last;
// 创建新节点,节点的pre指针last, next指向null
final Node<E> newNode = new Node<>(l, e, null);
// 将last指针指向新建的节点
last = newNode;
// 对应的头指针移动到新节点上
if (l == null)
first = newNode;
else
// 将尾指针指向新节点
l.next = newNode;
// 数量增加
size++;
// 修改次数增加
modCount++;
}

上述代码流程的梳理:

  1. 使用临时变量​​L​​来保存尾节点;
  2. 创建新节点​​new Node<>(l, e, null)​​,将新节点的​​prev​​指针指向尾节点,将新节点​​next​​节点指向​​null​​。
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
  1. 将​​last​​尾指针指向新节点
  2. 这是是分为两种情况,在空的链表上添加新节点;在非空链表上添加新节点
    i. 对于第一种情况:​​l==null​​(就是头指针和尾指针都指向​​null​​),需要将​​first​​头指针也指向新节点。
    ii. 对于第二种情况:​​l.next = newNode​​,需要将尾指针的​​next​​指向新节点,这就是可以将双向链表连接起来
  3. 最后增加链表节点数量增加一,操作修改数量增加一
5.1.2. ​​add(int index, E element)​​在指定位置添加节点

这里面也有两种情况需要考虑:

  • 增加元素的位置是头尾的情况
  • 增加元素的位置是非头尾的情况
/**
* 将指定的元素插入此列表中的指定位置。
* 将当前在该位置的元素(如果有的话)和任何后继元素右移(将其索引加一)。
*
* @param index 指定元素要插入的索引
* @param element 要插入的元素
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public void add(int index, E element) {
// 判断是index大于size
checkPositionIndex(index);
// 如果index等于size
if (index == size)
// 在链表节点尾部添加元素,这个我们上面已经分析了
linkLast(element);
else
// 在指定位置添加元素
linkBefore(element, node(index));
}


/**
* 返回指定元素索引处的(非空)节点。
*/
Node<E> node(int index) {
// assert isElementIndex(index);
// index位于size的前半部分,则从头节点开始遍历,返回index位置的节点
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
// index位于size的后半部分,则从尾节点开始遍历,返回index位置的节点
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}


/**
* 在非null节点succ之前插入元素e。
*/
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
// 使用pred保存index位置节点的prev指向元素
final Node<E> pred = succ.prev;
// 将新节点的next指向index位置所在的节点
// 将新节点的prev指向index位置所在的节点prev的指向
final Node<E> newNode = new Node<>(pred, e, succ);
// 再将index位置的节点prev指向新节点
succ.prev = newNode;
// 如果实在链表头插入,则需要将first指针指向新节点
if (pred == null)
first = newNode;
else
// 否则将就pred指向元素的next指向新节点
pred.next = newNode;
// 节点数量增加
size++;
// 修改次数增加
modCount++;
}

上述代码流程的梳理:

  1. 先要检测index是否合法​​index >= 0 && index <= size​
  2. 判断是否是在链表尾添加元素,是的话直接调用我们上面分析的​​linkLast(element)​​方法
  3. 否则就需要先获取​​index​​​位置的节点,然后调用​​linkBefore(element, node(index))​​此方法。
  4. 进入此方法之后,首先是先要使用一个临时变量去保存​​index​​​位置的节点​​prev​​保存的前一个节点。
  5. 创建一个新节点,将新节点的​​next​​​指向​​index​​​位置所在的节点,将新节点的​​prev​​​指向临时变量(这个临时变量保存的是就是​​index​​​位置节点​​prev​​指向的前一个节点)。
  6. 接着就将​​index​​​位置节点的​​prev​​指向刚才新创建的节点
  7. 然后就要判断是在表头添加(​​pred == null​​​),如果是只需要将​​first​​​指针指向新节点就可以;否者,就需要将临时变量(这个临时变量保存的是就是​​index​​​位置节点​​prev​​​指向的前一个节点)的​​next​​指向新节点。
  8. 最后将节点的数量++,修改次数++。
5.1.3. ​​addFirst(E e)​​​和​​addLast(E e)​​在链表头尾添加元素
/**
* 将指定的元素插入此列表的开头。
*
* @param e 要添加的元素
*/
public void addFirst(E e) {
linkFirst(e);
}

/**
* 将e链接为第一个元素。
*/
private void linkFirst(E e) {
// 先用一个临时变量f保存first指针
final Node<E> f = first;
// 创建一个新节点,将新节点的prev指向为null,将next指向临时变量f
final Node<E> newNode = new Node<>(null, e, f);
// 将fist指针指向新创建的节点
first = newNode;
// 这里需要判断f是否是null,如果是则说明当前链表的空链表,就需要把last指针也指向新节点
if (f == null)
last = newNode;
else
// 否则将f变量的prev指向新节点。这样就把链表就又串起来了
f.prev = newNode;
size++;
modCount++;
}

在表尾添加元素是一样的不展开分析了!

5.1.4. ​​addAll(int index, Collection<? extends E> c)​​将一个集合添加到链表中指定位置
/**
* 从指定位置开始,将指定集合中的所有元素插入此链表。
* 将当前位于该位置的元素(如果有)和任何后续元素右移(增加其索引)。
* 新元素将按指定集合的迭代器返回的顺序显示在列表中。
*
* @param index 从指定集合中插入第一个元素的索引
* @param c 包含要添加到此列表的元素的集合
* @return {@code true} if this list changed as a result of the call
* @throws IndexOutOfBoundsException {@inheritDoc}
* @throws NullPointerException if the specified collection is null
*/
public boolean addAll(int index, Collection<? extends E> c) {
// 判断index位置是否合适
checkPositionIndex(index);
// 将集合转成数组,并获取数组的长度
Object[] a = c.toArray();
int numNew = a.length;
// 数组长度为0,则返回false
if (numNew == 0)
return false;

Node<E> pred, succ;
// 这里判断是在链表的中间位置插入集合元素还是在链表的尾部添加元素,并将临时指针指向指定位置
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
// 通过迭代器将数组一一添加到集合中
for (Object o : a) {
// @SuppressWarnings 屏蔽某些编译时的警告信息,此处用来屏蔽强制类型转换
@SuppressWarnings("unchecked") E e = (E) o;
// 新建节点,将指向前一个元素指针赋值给新节点的prev指针,新节点的next指针指向向null
Node<E> newNode = new Node<>(pred, e, null);
// 此处用来应对在链表头位置添加元素的情况
if (pred == null)
first = newNode;
else
// 将上一个元素的next指针指向新节点
pred.next = newNode;
// 移动last指针指到最新的元素
pred = newNode;
}
// 将原来的index位置的节点连接到现在的链表上
if (succ == null) {
// 对于链表尾添加集合元素,直接将last指针移动到最后就可以。
last = pred;
} else {
// 对于在链表中间或者链表头添加集合,将最后一个新建节点的next指向succ(index
// 位置的节点),将succ(index位置节点指向前一个节点的指针指向pred节点)
pred.next = succ;
succ.prev = pred;
}
// 链表数量修改
size += numNew;
// 修改次数加一
modCount++;
return true;
}

5.2. 删除节点相关方法

5.2.1. ​​remove()​​删除第一个节点

这个方法调用​​unlinkFirst()​​核心方法

/**
* 检索并删除此链表的头(第一个元素)。
*
* @return 此链表的头
* @throws NoSuchElementException if this list is empty
* @since 1.5
*/
public E remove() {
return removeFirst();
}


/**
* 从此链表中删除并返回第一个元素。
*
* @return 此链表中的第一个元素
* @throws NoSuchElementException 链表是空
*/
public E removeFirst() {
final Node<E> f = first;
// 如果first指针是null,那说明整个链表是空的,会抛出NoSuchElementException异常
if (f == null)
throw new NoSuchElementException();
// 调用
return unlinkFirst(f);
}


/**
* 这个是链表删的核心方法
* 移除链接非空的第一个节点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;
// 如果next指针是null,就说明是空链表,则将last节点也置空
if (next == null)
last = null;
else
// 否则将新的头节点的前指针指向null
next.prev = null;
size--;
modCount++;
return element;
}

代码流程描述:

  1. 先获取​​first​​​指针,判断是否为​​null​​,空链抛出异常;
  2. 调用​​unlinkFirst(Node<E> f)​​方法
  3. 通过临时变量先保存​​first​​​指向的数据,并保存​​first​​​指向的​​next​​下一个节点
  4. 接着将头节点置空
  5. 判断头节点指向的下一个节点是否为​​null​​​,如果为​​null​​​的话则说明是空链,将​​last​​​指针指向​​null​​​;否者将头节点的前置指针指向​​null​​ ;
  6. 最后将节点数量减一,修改次数减一,并返回第三步保存的数据。

此外删除,最后一个节点的方法与此方法相识,不做分析。

5.2.2. 删除指定位置的元素

其中​​remove(int index)​​​、​​remove(Object o)​​​、​​removeFirstOccurrence(Object o)移除第一出现的元素​​​、​​removeLastOccurrence(Object o)移除最后一次出现的元素​​​这几个方法都是调用了​​unlink(Node<E> x)​​方法。

接下来我们看一下这个方法​​unlink(Node<E> x)​​:

/**
* 移除链接非空节点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;

// 前置指针为空,(说明这个位置是位于头节点),直接将头指针指向next
if (prev == null) {
first = next;
} else {
// 否则将前一个节点的后指针指向next节点,断开当前节点与前一节点的联系
prev.next = next;
x.prev = null;
}

// 后置指针为空,(说明这个位置是位于尾节点),直接将尾节点指向prev
if (next == null) {
last = prev;
} else {
// 否则将后一个节点的前置指针指向prev节点,断开当前节点与后一节点的联系
next.prev = prev;
x.next = null;
}

// 将元素为kong
x.item = null;
size--;
modCount++;
return element;
}

5.3. 获取某一个位置的节点

5.3.1. ​​indexOf(Object o)​​获取首次出现元素的位置,否者返回-1
/**
* 返回指定元素在此列表中首次出现的索引,如果此列表不包含该元素,则返回-1。
*
* @param o 搜索元素
* @return 指定的元素在此列表中第一次出现的索引;如果此列表不包含该元素,则为-1
*/
public int indexOf(Object o) {
// 初始为0
int index = 0;
// 判断元素是否为null
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
// 如果没有获取到则为 -1
return -1;
}
5.3.2. ​​lastIndexOf(Object o)​​:获取最后出现元素的位置,否者返回-1

这个方法与上面的方法是反方向查询,不做详细注释。

/**
* 返回指定元素在此列表中最后一次出现的索引;如果此列表不包含该元素,则返回-1。
*
* @param o 搜索元素
* @return 指定的元素在此列表中最后一次出现的索引;如果此列表不包含该元素,则为-1
*/
public int lastIndexOf(Object o) {
int index = size;
if (o == null) {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (x.item == null)
return index;
}
} else {
for (Node<E> x = last; x != null; x = x.prev) {
index--;
if (o.equals(x.item))
return index;
}
}
return -1;
}

5.4. 一些其他的方法

5.4.1 ​​toArray()​​按顺序将节点放到数组中
public Object[] toArray() {
// 先创造出一个size大小的数组
Object[] result = new Object[size];
int i = 0;
// 遍历将元素一个一个放进去
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}
5.4.2. 序列化方法

​LinkedList​​​中重写了​​writeObject​​​方法,​​ObjectOutputStream​​​中将调用​​ObjectStreamClass​​​里的方法通过反射根据方法名称来调用​​writeObject​​​方法,以​​LinkedList​​​中定义的方式来序列化链表中的元素和​​size​​字段。

将​​LinkedList​​写入流(序列化)

private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();

// Write out size
s.writeInt(size);

// Write out all elements in the proper order.
for (Node<E> x = first; x != null; x = x.next)
s.writeObject(x.item);
}

将​​LinkedList​​从流中读出来(反序列化)

/**
* Reconstitutes this {@code LinkedList} instance from a stream
* (that is, deserializes it).
*/
@SuppressWarnings("unchecked")
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();

// Read in size
int size = s.readInt();

// Read in all elements in the proper order.
for (int i = 0; i < size; i++)
linkLast((E)s.readObject());
}
5.4.3. 克隆方法
/**
* 返回此{@code LinkedList}的浅表副本。 (元素本身未被克隆。)
*
* @return 此{@code LinkedList}实例的浅表副本
*/
public Object clone() {
// 直接调用系统的clone方法构建一个LinkedList对象
LinkedList<E> clone = superClone();

// 为新的克隆的元素置于链表的初始状态
clone.first = clone.last = null;
clone.size = 0;
clone.modCount = 0;

// 将这些元素在试用add方法加入链表中
for (Node<E> x = first; x != null; x = x.next)
clone.add(x.item);

return clone;
}

@SuppressWarnings("unchecked")
private LinkedList<E> superClone() {
try {
return (LinkedList<E>) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}

六、迭代器的使用

这​​LinkedList​​​中使用迭代器或者增强​​for​​​循环是比使用原始​​for​​块很多的。

6.1. 迭代器的变量

// 本次遍历返回的元素节点
private Node<E> lastReturned;
// LinkedList中下一个元素的指针
private Node<E> next;
// 下一个元素的索引,每遍历一个元素,该值加1
private int nextIndex;
// 当前创建迭代器时list的修改次数
private int expectedModCount = modCount;

6.2. 迭代器的初始化

// 重指定位置开始迭代, 根据index的值,给next和nextIndex赋初始值
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}

6.3. 迭代器一些简单方法

// 判断是否仍然有元素, 如果nextIndex 大于 size的时候,就说明已经遍历到最后一个元素
public boolean hasNext() {
return nextIndex < size;
}

// 判断当前位置的上一个位置是否有元素
public boolean hasPrevious() {
return nextIndex > 0;
}

// 获取下一个遍历元素的下标(索引)
public int nextIndex() {
return nextIndex;
}

// 获取上一个元素的下标(索引)
public int previousIndex() {
return nextIndex - 1;
}

6.4. 迭代器在遍历的时候核心方法

这个迭代器是可以对所遍历的集合进行修改的,提供了​​add​​​、​​set​​​、​​remove​​方法。这里我们需要记住:避免并发修改异常的原则:在迭代过程中,不允许利用非迭代器的API对集合进行更新操作。

先看一下​​next​​方法

public E next() {
// 先检查是否发生并发修改的异常, 这里的expectedModCount保存的在使用迭代器最后修改的次数,当并发修改的时候,是无法再次更新到迭代器内部的最后修改次数,所以到expectedModCount和modCount是不一样的,就是发生了并发修改
checkForComodification();
// 检测是否包含下一个元素
if (!hasNext())
throw new NoSuchElementException();

// 把当前遍历的next节点,赋值给lastReturned,最后会将其返回
lastReturned = next;
// 更新下一个节点的位置
next = next.next;
// 然后将索引下标加一
nextIndex++;
return lastReturned.item;
}

接下来看一下遍历过程中修改的方法

// 利用迭代器对象移除一个节点,但是不能再创建迭代器之后就立即使用remove方法,需要在执行next()方法之后才可以
public void remove() {
// 先检查是否发生并发修改的异常 ==> fail-fast失效
checkForComodification();
// 当创建迭代器之后,lastReturned是null,直接执行是抛出异常
if (lastReturned == null)
throw new IllegalStateException();

// 获取lastReturned的下一个节点
Node<E> lastNext = lastReturned.next;
// 移除节点
unlink(lastReturned);
// 这里的可能出现多线程操作迭代器,在移除节点之后其他的线程已经操作了next的位置,这里就是判断当前迭代器对象的next是否与lastReturned是指向同一个元素,同一个元素的话只需要将next指向lastNext就可以,否则需要将索引减一
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}

public void set(E e) {
// 检测当期迭代的节点是否为null,为null说明迭代对象已经到尾
if (lastReturned == null)
throw new IllegalStateException();
// 接着检查是否发生并发修改的异常
checkForComodification();
// 最后修改元素
lastReturned.item = e;
}

// 这个add的方法是ListIterator接口提供的方法,利用迭代器对象添加一个元素到链表中
public void add(E e) {
// 先检查是否发生并发修改的异常,
checkForComodification();
// 检测当前迭代器是否为null,如果是null,则说明已经迭代到了链表的尾巴,此时执行add方法将在链表的尾追加元素e,否则在next位置,插入元素,这里两个方法可以看上面的源码分析
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
// 然后将索引和迭代器中修改次数加一
nextIndex++;
expectedModCount++;
}

过程看如图:

集合框架之LinkedList源码分析

七、​​LinkedList​​​作为​​Queue​​使用

7.1. 实现队列

7.1.1. 队列简述

所有的插入只能在表的一端进行,而所有的删除都在表的另一端进行的线性表。

表中允许插入的一端称为队尾(Rear),允许删除的一端称为队头(Front)。按先进先出(FIFO)的原则进行的。

7.1.2. 队列的常用方法

方法名

描述

add(E e)

向队列插入元素,在满队列中会抛出异常,不利于判断,更推荐offer

element()

返回队头元素,如果队列为空,则抛出一个​​NoSuchElementException​​异常

offer(E e)

向队列插入元素返回true,如果队列已满,则返回false

peek()

返回队头元素 , 如果队列为空,则返回null

poll()

移除并返问队头元素,队列为空,则返回null

remove()

移除并返回队头元素,队列为空,则抛出一个​​NoSuchElementException​​异常

​Deque​​​ 继承了 ​​Queue​​​接口的方法。当 ​​Deque​​​ 当做 队列使用时(​​FIFO​​​),添加元素是添加到队尾,删除时删除的是头部元素。在​​Linked​​实现的时候我们可以看到

7.1.3. 实例
public static void main(String[] args) {
Queue<String> queue = new LinkedList<>();
queue.add("张三");
queue.add("李四");
System.out.println("当前队列元素:" + queue);
System.out.println("获取队头元素:" + queue.element());
queue.offer("王五");
System.out.println("当前队列元素:" + queue);
System.out.println("获取队头元素:" + queue.peek());
System.out.println("移除队头元素:" + queue.poll());
queue.remove();
System.out.println("当前队列元素:" + queue);
}

7.2. 实现双端队列

7.2.1. 双端对列的简述:

我们可以再​​LinkedList​​​的​​UML​​​看到​​LinkedList​​​实现了​​Deque​​的双端队列。

双端队列是限定插入和删除操作在表的两端进行的线性表,是一种具有队列和栈的性质的数据结构。

7.2. 双端队列的特点:

双端队列中的元素可以从两端进队和出队,其限定插入和删除操作在链表的两端进行

集合框架之LinkedList源码分析

这里面的方法都是调用了我们刚才上面分析的那些方法。

八、​​LinkedList​​​作为​​Stack​​使用

8.1. 栈的简述

栈”通常是指“后进先出”(LIFO)的容器。有时栈也被称为叠加栈,因为最后压入栈的元素第一个弹出栈。

​LinkedList​​​具有能够实现栈的所有功能的方法,因此可以直接将​​LinkedList​​作为栈使用。

8.2. 栈的实现

class Stack<T> {
private Deque<T> stack = new LinkedList<>();

/**
* 入栈
* @param t
*/
public void push(T t) {
stack.push(t);
}

/**
* 出栈
*/
public void pop() {
stack.pop();
}

/**
* 获取栈顶元素
* @return
*/
public T peek() {
return stack.peek();
}

/**
* 判空
* @return
*/
public boolean empty() {
return stack.isEmpty();
}

/**
* 获取栈中元素数量
* @return
*/
public int size() {
return stack.size();
}

}

测试:

public static void main(String[] args) {
Stack<String> stringStack = new Stack<>();
for (String item : "2 0 2 0 鼠 年 快 乐".split(" ")) {
stringStack.push(item);
}
while (!stringStack.empty()) {
System.out.print(stringStack.peek());
stringStack.pop();
}
}

关于队列和栈部分重写一篇文章详细介绍!

九、喜欢的可以关注下公众号

集合框架之LinkedList源码分析