addFirst 内部其实调用的是 linkFirst:
public void addFirst(E e) { linkFirst(e); }
linkFirst 负责把新的节点设为 first,并将新的 first 的 next 更新为之前的 first。
private void linkFirst(E e) { final Node<E> f = first; final Node<E> newNode = new Node<>(null, e, f); first = newNode; if (f == null) last = newNode; else f.prev = newNode; size++; modCount++; }
addLast 的内核其实和 addFirst 差不多,就交给大家自行理解了。
2)招式二:删
我这个删的招式还挺多的:
remove():删除第一个节点
remove(int):删除指定位置的节点
remove(Object):删除指定元素的节点
removeFirst():删除第一个节点
removeLast():删除最后一个节点
remove 内部调用的是 removeFirst,所以这两个招式的功效一样。
remove(int) 内部其实调用的是 unlink 方法。
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
unlink 方法其实很好理解,就是更新当前节点的 next 和 prev,然后把当前节点上的元素设为 null。
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; if (prev == null) { first = next; } else { prev.next = next; x.prev = null; } if (next == null) { last = prev; } else { next.prev = prev; x.next = null; } x.item = null; size--; modCount++; return element; }
remove(Object) 内部也调用了 unlink 方法,只不过在此之前要先找到元素所在的节点:
public boolean remove(Object o) { if (o == null) { for (Node<E> x = first; x != null; x = x.next) { if (x.item == null) { unlink(x); return true; } } } else { for (Node<E> x = first; x != null; x = x.next) { if (o.equals(x.item)) { unlink(x); return true; } } } return false; }
这内部就分为两种,一种是元素为 null 的时候,必须使用 == 来判断;一种是元素为非 null 的时候,要使用 equals 来判断。equals 是不能用来判 null 的,会抛出 NPE 错误。
removeFirst 内部调用的是 unlinkFirst 方法:
public E removeFirst() { final Node<E> f = first; if (f == null) throw new NoSuchElementException(); return unlinkFirst(f); }
unlinkFirst 负责的就是把第一个节点毁尸灭迹,并且捎带把后一个节点的 prev 设为 null。
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; if (next == null) last = null; else next.prev = null; size--; modCount++; return element; }
3)招式三:改
可以调用 set() 方法来更新元素:
list.set(0, "沉默王五");
1
来看一下 set() 方法:
public E set(int index, E element) { checkElementIndex(index); Node<E> x = node(index); E oldVal = x.item; x.item = element; return oldVal; }
首先对指定的下标进行检查,看是否越界;然后根据下标查找原有的节点:
Node<E> node(int index) { // assert isElementIndex(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; } }
size >> 1:也就是右移一位,相当于除以 2。对于计算机来说,移位比除法运算效率更高,因为数据在计算机内部都是二进制存储的。
换句话说,node 方法会对下标进行一个初步判断,如果靠近前半截,就从下标 0 开始遍历;如果靠近后半截,就从末尾开始遍历。
找到指定下标的节点就简单了,直接把原有节点的元素替换成新的节点就 OK 了,prev 和 next 都不用改动。
4)招式四:查
我这个查的招式可以分为两种:
indexOf(Object):查找某个元素所在的位置
get(int):查找某个位置上的元素
indexOf 的内部分为两种,一种是元素为 null 的时候,必须使用 == 来判断;一种是元素为非 null 的时候,要使用 equals 来判断。因为 equals 是不能用来判 null 的,会抛出 NPE 错误。
public int indexOf(Object o) { int index = 0; 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++; } } return -1; }
get 方法的内核其实还是 node 方法,这个之前已经说明过了,这里略过。
public E get(int index) { checkElementIndex(index); return node(index).item; }
其实,查这个招式还可以演化为其他的一些,比如说:
getFirst() 方法用于获取第一个元素;
getLast() 方法用于获取最后一个元素;
poll() 和 pollFirst() 方法用于删除并返回第一个元素(两个方法尽管名字不同,但方法体是完全相同的);
pollLast() 方法用于删除并返回最后一个元素;
peekFirst() 方法用于返回但不删除第一个元素。
四、LinkedList 的挑战
说句实在话,我不是很喜欢和师兄 ArrayList 拿来比较,因为我们各自修炼的内功不同,没有孰高孰低。
虽然师兄经常喊我一声师弟,但我们之间其实挺和谐的。但我知道,在外人眼里,同门师兄弟,总要一较高下的。
比如说,我们俩在增删改查时候的时间复杂度。
也许这就是命运吧,从我进入师门的那天起,这种争论就一直没有停息过。
无论外人怎么看待我们,在我眼里,师兄永远都是一哥,我敬重他,他也愿意保护我。
好了,LinkedList 这篇就到这了。
如果大家有闲情逸致的话,建议手撕一下链表,可以从单向链表开始撕起。
希望大家能点赞下,给我注入一点点更新的动力。我也会不断地提升品质,给大家带来更硬核的技术文章,笔芯~