LinkedList 底层采用双向链表数据结构存储元素,由于链表的内存地址非连续,所以它不具备随机访问的特点,但由于它利用指针连接各个元素,所以插入、删除元素只需要操作指针,不需要移动元素,故具有增删快、查询慢的特点。 它也是一个非线程安全的集合。 LinkedList适用于先进先出和先入后出的场景,在队列源码中被频繁使用。
整体架构
LinkedList 底层数据结构是一个双向链表,整体结构如下图所示:
关于上面的双向链表需要注意的是:
1:当链表中没有数据时,first 和 last 是同一个节点,前后指向都是 null。
2:因为是个双向链表,只要机器内存足够强大,是没有大小限制的。
链表中的元素叫做 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;
}
}
LinkedList的主要操作
//初始化方法1
HashSet<Object> hashSet = new HashSet<>();
hashSet.add(111);
hashSet.add(222);
LinkedList<Object> linkedList1 = new LinkedList<>(hashSet);
//初始化方法2
LinkedList<Object> linkedList = new LinkedList<>();
boolean test = linkedList.add("test");
//单向迭代器
Iterator<Object> iterator = linkedList.iterator();
while (iterator.hasNext()) {
iterator.remove();
Object next = iterator.next();
System.out.println(next);
}
//双向迭代器
ListIterator<Object> previousIterator = linkedList.listIterator(0);
while (previousIterator.hasPrevious()) {
Object previous = previousIterator.previous();
System.out.println(previous);
}
Object o2 = linkedList.get(0);
boolean empty = linkedList.isEmpty();
Object o1 = linkedList.removeFirst();
Object o = linkedList.remove(0);
int num = linkedList.size();
System.out.println(num);
新增节点
追加节点时,可以选择追加到链表头部,还是追加到链表尾部,add 方法默认是从尾部开始追加,addFirst 方法是从头部开始追加,我们分别来看下两种不同的追加方式:
add方法:
/**
* Links e as last element.
*/
void linkLast(E e) {
final Node<E> l = last;// 将尾节点暂存在节点I中
final Node<E> newNode = new Node<>(l, e, null);// 通过有参构造函数初始化新增节点
last = newNode;// 将新增节点变成尾节点
if (l == null)
first = newNode;//如果链表为空,头尾节点都是同一个节点,均为新建的节点
else
l.next = newNode;// 如果不为空,将前尾节点的next节点指向新增节点
size++;// 链表大小加一
modCount++;// 版本加一
}
使用动态图清晰的展示一下新增节点的操作:
addFirst方法:
/**
* Links e as first element.
*/
private void linkFirst(E e) {
final Node<E> f = first;// 将头节点暂存在f中
final Node<E> newNode = new Node<>(null, e, f);//通过有参构造初始化新增到链头部的节点
first = newNode;// 将新增节点赋值给first节点
if (f == null)
last = newNode;// 如果链为空,头尾节点都是同一个节点,均为新建的节点
else
f.prev = newNode;// 不为空的话,将前头结点的前置节点指向新增节点(新的头节点)
size++;
modCount++;
}
头部追加节点和尾部追加节点非常类似,只是前者是移动头节点的 prev 指向,后者是移动尾节点的 next 指向。
删除节点
节点删除的方式和追加类似,可以选择从头部删除,也可以选择从尾部删除,删除操作会把节点的值,前后指向节点都置为 null,帮助 GC 进行回收。
/**
* Unlinks non-null first node 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;// 帮助GC回收垃圾
f.next = null; // help GC
first = next;// 头结点的下一个结点成为头结点
if (next == null)
last = null;// 如果链表在删除头结点之前只有一个节点,那么删除后链表为空
else
next.prev = null;// 如果不为空,新的头节点的前置节点指向空
size--;
modCount++;
return element;//返回删除节点的值
}
从尾节点删除节点也是一样的步骤,从源码中发现,链表结构的节点删除、新增代码都非常简单,因此LinkedList新增和删除速度很快。
查询节点
链表查询某一个节点是比较慢的,需要挨个循环查找才行,查询方法代码如下:
/**
* Returns the (non-null) Node at the specified element index.(根据链表索引位置查询节点)
*/
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {// 如果index处于队列的前半部分,从头结点开始遍历(size>>1是除以二)
Node<E> x = first;
for (int i = 0; i < index; i++)// 循环到index的前一个节点
x = x.next;
return x;
} else {// 如果index处于队列的后半部分,从尾节点开始遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)// 遍历到index后一个节点
x = x.prev;
return x;
}
}
LinkedList 并没有采用从头循环到尾的做法,而是采取了简单二分法,首先看看 index 是在链表的前半部分,还是后半部分。如果是前半部分,就从头开始寻找,反之亦然。通过这种方式,使循环的次数至少降低了一半,提高了查找的性能,这种思想值得借鉴。
方法对比
LinkedList 实现了 Queue 接口,使得 LinkedList 可以用作双端队列。在新增、删除、查询等方面增加了很多新的方法,这些方法在平时特别容易混淆,在链表为空的情况下,返回值也不太一样,列一个表格,方便记录:
方法含义 | 返回异常 | 返回特殊值 | 底层实现 |
---|---|---|---|
新增 | add(e) | offer(e) | 底层实现相同 |
删除 | remove() | poll(e) | 链表为空时,remove 会抛出异常,poll 返回 null。 |
查找 | element() | peek() | 链表为空时,element 会抛出异常,peek 返回 null。 |
迭代器
因为 LinkedList 要实现双向的迭代访问,所以使用 Iterator 接口肯定不行了,因为 Iterator 只支持从头到尾的访问。Java 设计者新增了一个迭代接口,叫做:ListIterator,这个接口提供了向前和向后的迭代方法,如下所示:
迭代顺序 | 方法 |
---|---|
从尾到头迭代 | hasPrevious、previous、previousIndex |
从头到尾迭代 | hasNext、next、nextIndex |
上图中LinkedList调用的是ListIterator,源码如下所示:
public ListIterator<E> listIterator(int index) {// 从下标index处开始迭代输出
checkPositionIndex(index);// 判断index索引是否存在
return new ListItr(index);// 返回迭代器实体
}
上述源码中,return new ListItr(index);
返回双向迭代器操作实体,部分代码如下:
private class ListItr implements ListIterator<E> {// 双向迭代器
private Node<E> lastReturned;// 上一次执行next()或previous()时节点位置
private Node<E> next;// 下一个节点
private int nextIndex;// 下一个节点的位置
private int expectedModCount = modCount;// 期望版本号=目前最新版本号
ListItr(int index) {// 有参构造函数
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);// 获取下一个节点
nextIndex = index;//获取下一个要迭代输出节点的索引
}
......
从头到尾方向的迭代:
public E next() {//迭代输出操作函数
checkForComodification();// 检查期望版本号是否有变化
if (!hasNext())// 再次检查是否有下一个结点
throw new NoSuchElementException();
lastReturned = next;// 将next节点(初始化迭代器时index指向node)赋值给lastReturned(存储需要迭代输出的节点)
next = next.next;// 将要输出的当前节点的next节点赋值给next本身
nextIndex++;// 节点下标加一
return lastReturned.item;// 返回存储在lastReturned中的迭代输出节点值
}
从尾到头方向的迭代:
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
//next为空场景:1.第一次迭代到了尾节点 2.上一次操作把尾节点删除了
lastReturned = next = (next == null) ? last : next.prev;// next不为空直接取前置节点
nextIndex--;// 节点索引减一
return lastReturned.item;
}
迭代器删除元素
public void remove() {// 迭代删除方法
checkForComodification();
if (lastReturned == null)//如果lastReturned为空说明迭代器没有初始化,迭代器中没有值,或者lastReturned时尾节点的next
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;//将要删除节点的next节点存储在lastNext中
unlink(lastReturned);// 删除当前节点
if (next == lastReturned)// 当从尾到头迭代的时候,第一次迭代要删除最后一个节点
next = lastNext;// lastReturned是尾节点,lastNext也为空,所以next也是空
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}