LinkedList
底层数据结构是双向链表,可以存储任何元素(包括 null)。它也实现了 Deque
接口,可以将它当做双向队列使用。
初始化
它并没有任何初始化。双向链表依靠 first
和 last
两个节点维护:
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
对比 LinkedHashMap
发现,双向链表使用头尾节点维护,LinkedList
的节点类型为 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;
}
}
整个数据结构大概如此了。
存取操作
来看看新增操作:
add(E)
public boolean add(E e) {
linkLast(e);
return true;
}
// 将新增节点作为最后一个节点
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
add(int,E):
public void add(int index, E element) {
checkPositionIndex(index);
// 作为最后一个节点
if (index == size)
linkLast(element);
else
// 插入 index 节点之前
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 对于 add 或者 iterator 操作,index 是否是一个有效的参数
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
// 返回特殊元素索引的节点
Node<E> node(int index) {
// assert isElementIndex(index);
// 基于 size 对半查找索引的节点
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;
}
}
这里需要注意,使用带索引的 add 新增操作会有一个查找过程,并且会对索引进行校验,索引应当大于等于0且小于等于 size,这与 set 操作略有不同。
**set(E) **:
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
严格来说,set 属于替换值操作,并不属于新增操作,所以它在检查索引时,索引必须存在,即大于等于0,小于 size 。
对于底层数据结构是双向链表的 LinkedList
来说,它在按索引新增或者替换值时,需要先查找到节点(size 大小一般的循环),而对于底层数据结构是数组的 ArrayList
来说,在按索引新增时,会导致数组复制操作,在替换的时候,则非常快,不会有任何额外的操作。两者的共同点就是新增的索引可以等于 size ,而替换操作的索引则不能。
删除操作
remove 操作:
// 循环链表,查找到待移除对象,则移除
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;
}
// 查找到当前索引所在节点,移除该节点
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
相对于 ArrayList
来说,两者在移除对象时,都有一个循环操作,需要注意,这里的移除仅仅是移除所找到的第一个元素。在基于索引的移除时,ArrayList
会多一个数组的复制操作,而 LinkedList
则多一个查找操作,并且由于 LinkedList
还实现了 Deque
接口,所以它额外支持 removeFirst
,removeLast
快捷操作。
迭代器
在了解底层数据结构之后,对于迭代器的操作,就变为对于底层数据结构的操作。LinkedList
和 ArrayList
的迭代器都是 “快速失败” 的。在遇到并发修改集合结构时,会尽最大努力抛出 并发修改异常。这依赖于它们内部维护的 modCount
计数,在创建迭代器的时候,会将该值赋值给迭代器的 expectedModCount
,在迭代器的后续操作中会比较 modCount
和 expectedModCount
(集合的新增,或者删除等会修改 modCount
),如果不等于,则抛出 ConcurrentModificationException
。
注意:并发修改时,只是尽最大努力抛出,我们并不能依赖该异常,下面这种情况则不会抛出异常:
List<String> linkedList = new LinkedList<>();
linkedList.add("11s");
linkedList.add("123");
linkedList.add("13s");
Iterator<String> iterator = linkedList.iterator();
while(iterator.hasNext()){
String value = iterator.next();
System.out.println(value);
if(value.equals("123")){
linkedList.remove("11s");
}
}
这是一种很典型的情况,在遍历到倒数第二个元素以后,进行更改容器结构的操作,这并不会导致并发修改异常抛出。这是因为并发修改异常会在 next 方法抛出,但按如上操作,在下一次进行 hasNext
判断时,循环就结束了,也就无法执行到 next 方法了。
这是一种错误的示范,并不代表你就可以这样去进行修改操作。使用迭代器的 remove 可以移除元素,如果还想进行新增操作,则可以使用 list 的
ListIterator
迭代器。
总结
最近看了几篇集合或者 Map
,它们的很多特性都是由于底层数据结构决定的,所以了解底层数据结构之后,对于在什么场景下,使用什么容器就更加清楚了。
LinkerList
相比 ArrayList
更加适用于删除或者修改操作比较多的场景,它会比 ArrayList
少一些数组复制的操作。但对于查找某个特定索引位置的元素,ArrayList
则拥有更快的速度。不过对于遍历,两者应该都可以。
推荐博文
我与风来
认认真真学习,做思想的产出者,而不是文字的搬运工
错误之处,还望指出