void linkLast(E e) {
final Node l = last;
// 构造新的节点,上一节点指向原来的last
final Node 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,中间插入的时候,需要分两步:
-
node(int index)方法获取到index位置的元素succ
-
linkBefore(E e, Node succ)将需要插入的元素element连接到succ后面
node方法是一个频繁被调用的方法,LinkedList 的很多操作都依赖于该方法查找到对应的元素。根据索引 index 获取元素时,因为双向链表的支持前后遍历,所以进行了位置判断,index < (size >> 1),与中间位置比较,靠前则前序遍历,否则后序遍历。
Node node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) { //前序遍历
Node x = first;
for (int i = 0; i < index; i )
x = x.next;
return x;
} else {
Node 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 succ) {
// assert succ != null;
final Node pred = succ.prev;
final Node 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 f = first;
final Node 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 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 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 f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E getLast() {
final Node 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 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 x) {
// assert x != null;
final E element = x.item;
// 下一节点
final Node next = x.next;
// 前一节点
final Node 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 f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node f) {
// assert f == first && f != null;
final E element = f.item;
final Node 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
【一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
也是类似,这里各位看官自行进行体会。
public E removeLast() {
final Node l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
private E unlinkLast(Node l) {
// assert l == last && l != null;
final E element = l.item;
final Node 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 x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node 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 x = last; x != null; x = x.prev) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
// 后序遍历
for (Node x = last; x != null; x = x.prev) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}