LinkedList
底层数据结构是双向链表,如图。链表中没数据时,first和last是同一个结点,前后指向null。因为是个双向链表,只要机器内存足够大,没有大小限制。
内部有一个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;
}
}
1、 新增
追加节点时,我们可以选择追加到链表头部,还是追加到链表尾部,add 方法默认是从尾部开始追加,通过移动尾节点的 next 指向,addFirst 方法是从头部开始追加,通过移动头节点的 prev 指向。
//从尾部增加
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
//把尾节点数据暂存
final Node<E> l = last;
//新建节点,l是新节点的前驱,e为要新增的节点,新增节点的后一个节点为null
final Node<E> newNode = new Node<>(l, e, null); //(前驱、本身、后继)
//把newNode追加到尾部
last = newNode;
//链表空与不空两种情况
if (l == null)
first = newNode;
else
l.next = newNode;
//大小与版本的修改
size++;
modCount++;
}
//从头部增加
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
//头节点赋给临时变量
final Node<E> f = first;
//新建节点,新节点前驱为null,e是新建节点,f为后继
final Node<E> newNode = new Node<>(null, e, f); //(前驱、本身、后继)
first = newNode;
//判断链表空不空
if (f == null)
last = newNode;
else
f.prev = newNode;
//修改大小与版本
size++;
modCount++;
}
2、 删除
节点删除的方式和追加类似,我们可以选择从头部删除,也可以选择从尾部删除,删除操作会把节点的值,前后指向节点都置为 null,帮助 GC 进行回收。贴一个从头部删除
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
//拿出头节点的值作为方法的返回值
final E element = f.item;
//拿出头节点的下一个节点
final Node<E> next = f.next;
//帮助GC回收
f.item = null;
f.next = null; // help GC
//头节点的下一个节点成为头节点
first = next;
//如果 next 为空,表明链表为空
if (next == null)
last = null;
else
//链表不为空,头节点的前一个节点指向 null
next.prev = null;
//修改大小与版本
size--;
modCount++;
return element;
}
3、 查询
链表查询某一个节点是比较慢的,需要挨个循环查找才行。
Node<E> node(int index) {
//如果 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;
}
LinkedList 并没有采用从头循环到尾的做法,而是采取了简单二分 法,首先看看 index 是在链表的前半部分,还是后半部分。如果是前半部分,就从头开始寻 找,反之亦然。通过这种方式,使循环的次数至少降低了一半,提高了查找的性能。
4、 方法对比
方法含义 | 返回异常 | 返回特殊值 | 底层实现 |
---|---|---|---|
新增节点 | add(e) | offer(e) | 底层实现相同 |
删除节点 | remove(e) | poll(e) | 链表为空时,remove 会抛出异常,poll 返回 null。 |
查找节点 | element(e) | peek() | 链表为空时,element 会抛出异常,peek 返回 null。 |
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
LinkedList 也实现了 Deque 接口,对新增、删除和查找都提供从头开始,还是从尾开始两种方向的方法,比如 remove 方法,Deque 提供了 removeFirst 和 removeLast 两种方向的使用方式,但当链表为空时的表现都和 remove 方法一样,都会抛出异常。
5、 迭代器
因为 LinkedList 要实现双向的迭代访问,所以我们使用 Iterator 接口肯定不行了,因为 Iterator 只支持从头到尾的访问。Java 新增了一个迭代接口,叫做ListIterator,这个接口提供了向前和向后的迭代方法。
迭代顺序 | 方法 |
---|---|
从头到尾迭代方法 | hasNext、next、nextIndex |
从尾到头迭代方法 | hasPrevious、previous、previousIndex |
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned; //上一次执行 next() 或者 previos() 方法时的节点位置
private Node<E> next; //下一个节点
private int nextIndex; //下一个节点的位置
private int expectedModCount = modCount; //期望版本号与目标版本号
public boolean hasNext() {
return nextIndex < size; // 下一个节点的索引小于链表的大小,就有
}
public E next() {
//检查期望版本号有无发生变化
checkForComodificaction();
//再次检查
if (!hasNext())
throw new NoSuchElementException();
//把上一个节点位置改为当前节点
lastReturned = next;
// next 是下一个节点了,为下次迭代做准备
next = next.next;
nextIndex++;
//返回节点值
return lastReturned.item;
}
//从尾到头迭代
public boolean hasPrevious() {
return nextIndex > 0; // 如果上次节点索引位置大于 0,就还有节点可以迭代
}
// 取前一个节点
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
// next 为空场景:1:说明是第一次迭代,取尾节点(last);2:上一次操作把尾节点删除掉了
// next 不为空场景:说明已经发生过迭代了,直接取前一个节点即可(next.prev)
lastReturned = next = (next == null) ? last : next.prev;
// 索引位置变化
nextIndex--;
return lastReturned.item;
}
//迭代器删除
public void remove() {
checkForComodification();
// lastReturned 是本次迭代需要删除的值,分以下空和非空两种情况:
// lastReturned 为空,说明调用者没有主动执行过 next() 或者 previos(),直接报错
// lastReturned 不为空,是在上次执行 next() 或者 previos()方法时赋的值
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
//删除当前节点
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
6、 面试问题
(1)ArrayList 和 LinkedList 有何不同?
答:先从底层数据结构开始说起,然后以某一个方法为突破口深入,比如:最大的不同是两者底层的数据结构不同,ArrayList 底层是数组,LinkedList 底层是双向链表,两者的数据结构不同也导致了操作的 API 实现有所差异,拿新增实现来说,ArrayList 会先计算并决定是否扩容,然后把新增的数据直接赋值到数组上,而 LinkedList 仅仅只需要改变插入节点和其前后节点的指向位置关系即可。
(2)ArrayList 和 LinkedList 应用场景有何不同
答:ArrayList 更适合于快速的查找匹配,不适合频繁新增删除,像工作中经常会对元素进行匹 配查询的场景比较合适,LinkedList 更适合于经常新增和删除,对查询反而很少的场景。
(3)ArrayList 和 LinkedList 两者有没有最大容量
答:ArrayList 有最大容量的,为 Integer 的最大值,LinkedList 底层是双向链表,理论上可以无限大,但源码中,LinkedList 实际大小用 的是 int 类型,这也说明了 LinkedList 不能超过 Integer 的最大值,不然会溢出。
(4)ArrayList 和 LinkedList 是如何对 null 值进行处理的
答:ArrayList 允许 null 值新增,也允许 null 值删除。删除 null 值时,是从头开始,找到第一个值是 null 的元素删除,LinkedList 也是允许null值的新增和删除的。
(5)ArrayList 和 LinedList 是线程安全的么,为什么?
答:当两者作为非共享变量时,比如说仅仅是在方法里面的局部变量时,是没有线程安全问题 的,只有当两者是共享变量时,才会有线程安全问题。