之前的文章中,我们谈到了ArrayList的源码分析,在今天的文章中,我们来看一种和ArrayList非常相似的Java容器——LinkedList。
谈到LinkedList,我们就不得不谈到它的底层数据结构——链表。
private static class Node {
E item;
Node next;
Node prev;
Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
上面的代码就是LinkedList链表中节点的实现,我们来看一下每一个Node节点中存储一个值item
,还有指向前后节点的prev
和next
,我们通过一张图来描述一下节点之间的关系。由下图我们可以知道LinkedList的底层实现是双向链表。
transient int size = 0;
transient Node first;
transient Node last;
public LinkedList() {
}
很尴尬,构造方法什么都没有。就谈谈三个成员变量吧,说实话,我觉得从变量名大家都能看出来size
是LinkedList的长度,first
和last
分别是LinkedList的头节点和尾节点。我们看一下调用了构造方法之后的运行时数据区的情况。
我们首先尝试添加写一个添加元素的代码。
public static void main(String[] args) {
LinkedList list = new LinkedList<>();
list.add("路人甲");
list.add("路人乙");
}
add
方法中调用了一个linkLast
方法进行真实的链表节点插入操作,我们尝试插入第一个元素,传入一个String类型的值“路人甲”,看一下linkLast
进行了哪些操作。
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node l = last;
final Node newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
由于构造方法中没有对成员变量做任何操作,所以last
的值是null
,也就是说l
被赋值为null
。接着创建了一个新的节点,这个节点的prev
指向l
(也就是为空),next
也为空。接着LinkedList的last
指向了我们新创建的节点,然后我们需要判断l
是否为空,这里是空值,所以我们将first
也指向新节点。最后将链表长度加1,操作次数加1。
我们尝试再添加一个元素“路人乙”,由于插入了第一个元素,last
指向了第一个元素,也就是l
指向第一个元素。我们再创建一个新的node节点,prev
指向l
(也就是第一个节点),next
仍然为空,再将last
指向第二个节点。此时由于l
已经不为空,所以判断语句会走else,也就是将第一个节点的next
指向第二个节点,再次对链表长度和修改次数加1。看一下这时候内存区域的状态。
再次添加元素的操作基本和第二次添加元素差不多,就是引用指向节点和链表长度的变化,这里不再赘述。
我们来尝试一下删除元素“路人甲”。
public static void main(String[] args) {
LinkedList list = new LinkedList<>();
list.add("路人甲");
list.add("路人乙");
list.remove("路人甲");
}
查看一下remove
的源码部分,首先判断传入的元素是否为空,若为空则从链表头部开始查找节点值为空的元素,如果找到了就把进入unlink
方法并返回true;若不为空就从链表头部开始查找节点值为传入的值的元素,如果找到了就把进入unlink
方法并返回true;如果都没有找到传入的元素就返回false。由于传入的元素是“路人甲”,应该进入else循环。
public boolean remove(Object o) {
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;
}
我们再来看一下unlink
方法的实现,代码有点长,希望大家耐心看完。由于我们删除的是“路人甲”,是链表的第一个元素,所以prev
为空,first
指向路人甲的下一个节点,而next
是不为空的,所以路人乙的节点的prev
指向一个空值,并将待删除元素的next
置为空,元素的值也被置为空,链表长度减1,操作次数加1,返回删除元素的值。
那么如果删除的是“路人乙”呢?路人乙位于链表尾部,因此next
为空,prev
不为空,路人乙的前一个节点的next
也就指向null
,当前节点的prev
也置为空了。接着把last
置为空,并将待删除元素的值置为空,链表长度减1,操作次数加1,返回删除元素的值。
E unlink(Node x) {
// assert x != null;
final E element = x.item;
final Node next = x.next;
final Node 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;
}
以上讲了一些基础的添加与删除元素的操作,LinkedList还有一些其他的增/删元素的操作,比如在指定位置插入元素等,在指定位置操作元素就需要进行下标校验,原理上没有太大区别,大家可以自行阅读一下源码。