前言
上篇文章Java源码解析之ArrayList我们分析了ArrayList的jdk源码实现,本文来看一下它的兄弟LinkedList的源码。
原理
和ArrayList底层使用数组不同,LinkedList底层使用双向链表实现,这直接决定了LinkedList随机插入、删除效率很高,不需要像数组那样拷贝和移位,但随机查找的效率就比较低了,因为需要通过遍历链表来定位元素。
LinkedList在内部定义了一个节点类,用来构建双向链表
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;
}
}
复制代码
同时维护了两个首尾指针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;
复制代码
构建出来的双向链表如下图
下面我们就通过图文结合的方式来解析其中几个重要方法,看图很好理解各个链表操作的效果
add(index, element)方法
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
复制代码
当index==size时,直接在链表尾部添加元素,执行linkLast(element);
方法
void linkLast(E e) {
//将l指针指向尾节点
final Node<E> l = last;
//构建新节点
final Node<E> newNode = new Node<>(l, e, null);
//将last指针指向新节点
last = newNode;
if (l == null)
first = newNode;
else
//将原尾节点的next指针指向新节点
l.next = newNode;
size++;
modCount++;
}
复制代码
final Node<E> newNode = new Node<>(l, e, null);
这行代码会在链表尾部构建一个新节点newNode,此时整个链表如下:
l.next = newNode;
修改l节点的next指针,l节点也就是原尾节点,此时原尾节点的next指针会指向我们新添加的节点,这样就将新节点挂到了链表的尾部,效果如下:
我们再来看下,如果新节点不是添加在尾部,而是加在中间是如何实现的呢?
当index!=size时,会在index位置处插入节点,执行的是linkLast(element);
方法
//succ入参传进来的index位置的节点node(index),后面称之为succ节点
void linkBefore(E e, Node<E> succ) {
// 将pred指针指向index节点的前节点
final Node<E> pred = succ.prev;
//构建一个新节点,prev指针指向pred,next指针指向succ
final Node<E> newNode = new Node<>(pred, e, succ);
//将succ节点的prev指针指向新节点
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
复制代码
此时,我们假设传进来的index是1,也就是在第二个节点前添加新节点
final Node<E> newNode = new Node<>(pred, e, succ);
构建完新节点之后的效果如下:
succ.prev = newNode;
将succ节点的prev指针指向新节点
pred.next = newNode;
将pred指向的节点的next指针指向新节点
走完上面两行代码之后,就完成了新节点的添加,将newNode节点插入到了index所在的位置,整个链表如下图:
get(index)方法
讲完了插入方法,我们再来看如何从链表中获取元素,下面是get方法
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
复制代码
一共就两行代码,第一行检查元素越界,不多说了,第二行是重点,通过node方法来定位节点
Node<E> node(int index) {
// assert isElementIndex(index);
//判断索引位置是否小于size的一半,size>>1等效size/2
if (index < (size >> 1)) {
//如果索引小于size的一半,从头部开始遍历
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//如果索引小于size的一半,从尾部开始遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
复制代码
上面的代码很通俗易懂,就是通过Node的next方法往后遍历,或者通过prev方法往前遍历,这里的一个亮点在于判断了index是否超过了链表长度的一半,从而决定是往前还是往后遍历,最多只需遍历半条链表,提高效率。
以上,我们通过分析add方法和get方法,知道了LinkedList是如何通过链表来添加元素和获取元素的,实现过程并不是很复杂,同理,对于删除元素、更新元素等其他方法,我们只要通过画图的方式就很容易搞懂每一步的操作,此处强烈建议对于链表操作可以画图进行理解,如果光看代码可能不是很好理解,一画图边一目了然
好了,今天的文章就是这些内容,如果有不同看法或疑问欢迎随时和我讨论