一、LinkedList的概述与特点
LinkedList与ArrayList一样都实现了List接口,但是在底层上却截然不同。也许在平时中它的使用频率不及ArrayList,但是相较于ArrayList,LinkedList也具有独有的特点,因此知道它的实现方式以及在特定场合中使用它是很有必要的。
LinkedList的特点如下:
(1)底层为一个双向链表,存储空间不连续,增删快,查找慢。
(2)属于List,有序存储,存储元素可重复,允许null元素存储。
(3)不支持同步,线程不安全。
二、继承体系
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
LinkedList继承AbstractSequentialList抽象父类,实现了List、Deque、Cloneable、Serializable接口。
其中:
(1)AbstractSequentialList抽象类:提供了顺序访问的数据结构(例如链表)的一些工作。上一篇文章提到,ArrayList实现了随机访问的接口,遍历时使用for循环更快。而LinkedList底层的链表属于顺序存储结构,使用迭代器Iterator更快,所以这个抽象类提供的方法都是使用迭代器操作的一些方法。
(2)List接口:定义了对List的一些操作规范。
(3)Deque接口:Deque是双端队列英文的缩写,支持在两端插入和移除元素。
(4)Cloneable接口:实现此接口可以调用clone()方法,LinkedList的克隆是浅克隆。
(5)Serializable接口:代表可序列化。
三、重要属性
(1)private static class Node<E>
其实严格的说Node并不是一个属性,而是内部类,但是由于它是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;
}
}
可以看出Node是一个双向的节点,所以由此我们可以看出LinkedList的底层数据结构是一个双向链表。
(2)transient Node<E> first;
指向第一个节点的指针。
(3)transient Node<E> last;
指向最后一个节点的指针。
(4) transient int size = 0;
链表的长度。
四、 构造方法
LinkedList有两个构造方法:LinkedList()
、LinkedList(Collection<? extends E> c)
。代码:
/**
* 创建一个空列表
*/
public LinkedList() {
}
/**
* 创建一个指定集合中的元素的列表,
* 这些元素按照它们在迭代器中的顺序返回
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
LinkedList()
:可以看到无参的构造方法里什么也没做,所以它只是创建了一个空的列表而已。
LinkedList(Collection<? extends E> c)
:创建包含一个指定集合中的元素的列表。
五、常用方法
LinkedList底层是个双向链表,它的源码也都是在操作这个双线链表,难度不大。
(1)增加方法
LinkedList提供的增加方法有:
public boolean add(E e) //将指定元素添加到此列表的结尾
public void add(int index, E element) //将指定的元素插入此列表中的指定位置
public void addFirst(E e) //将指定的元素插入到列表开头
public void addLast(E e) //将指定的元素插入到列表结尾
public boolean addAll(Collection<? extends E> c) //将集合c中的元素添加到此列表的结尾
public boolean addAll(int index, Collection<? extends E> c) //将集合c中的元素添加到此列表的指定位置
我们重点看一下add(int index, E element)
的实现方式:
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
整体的思路是:首先检查index是否越界,然后进行判断,如果index等于链表现在的长度,那么就在链表的最后添加,否则在指定位置添加。
checkPositionIndex(index)
的代码如下:
private void checkPositionIndex(int index) {
//如果位置越界,则抛异常
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isPositionIndex(int index) {
//index合法的范围是[0,size]
return index >= 0 && index <= size;
}
在链表尾部添加元素调用的是linkLast(element)
:
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++;
}
先用节点l将之前的last节点存一下。然后创建一个前指针指向l,后指针为null的新节点,将last指针指向新节点。如果之前的尾部节点为空,说明链表是空的,那么让first节点等于新节点,否则让节点l的后指针指向新节点。最后让size+1,操作次数modCount+1。
如果不是在尾部添加,则需要先拿到index对应的节点,然后再进行添加。根据index找节点调用的是node(index)
:
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;
}
}
这里用了一种对半查找的方法,先判断index是前一半还是后一半,然后再决定从fist开始查找还是从last开始查找。最后将查找到的节点返回。
然后调用的是linkBefore
方法,作用是在指定节点之前插入元素:
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
比较简单,无非就是:(a)让指定节点的前指针指向要插入的新节点。(b)让新节点的后指针指向当前节点,新节点的前指针指向指定节点的前节点。©最后让指定节点的前节点的后指针指向新节点。
其他的add方法也是类似的过程,可以自行查看源码。
(2)删除方法
LinkedList提供的删除方法有:
public boolean remove(Object o) //从列表中删除第一次出现的指定元素
public E remove(int index) //删除列表中指定位置的元素
public E removeFirst() //从列表中删除并返回第一个元素
public E removeLast() //从列表中删除并返回最后一个元素
public boolean removeFirstOccurrence(Object o) //从列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)
public boolean removeLastOccurrence(Object o) //从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)
我们重点看一下remove(int index)
:
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
代码也比较简单,checkElementIndex(index)
也是检查index是否越界,与前文的逻辑一样。node(index)
前面也说过了,重点看一下unlink
方法。
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;
}
主要逻辑是拿到指定节点的数据、前节点和后节点,让前节点的后指针指向后节点,后节点的前指针指向前节点,这个过程还需要判断一下指定节点是否为first/last节点。最后把指定节点的数据与前后指针都指向null(方便JVM回收)。让链表长度减一,操作数加一,并将数据返回。
(3)修改方法
修改方法比较简单,直接看代码:
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
检查index是否越界,得到要修改的节点,将它的值改变,并把原先的值返回即可。
(4)查找方法
涉及到查找的方法有:
public E get(int index) //返回此列表中指定位置的元素。
public E getFirst() //返回此列表中的第一个元素。
public E getLast() //返回此列表中的最后一个元素。
这部分代码也比较简单,不再赘述。
(5)Deque方法
前面提到LinkedList实现了Deque接口,提供了一些关于双端队列的方法:
public E peek() //检索但不删除此列表的头(第一个元素)
public E poll() //检索并删除此列表的头(第一个元素)
public E peekFirst() //检索但不删除此列表的第一个元素
public E peekLast() //检索但不删除此列表的最后一个元素
public E pollFirst() //检索并删除此列表的第一个元素
public E pollLast() //检索并删除此列表的最后一个元素
public void push(E e) //将元素压入此列表表示的堆栈
public E pop() //从此列表表示的堆栈中弹出一个元素
public boolean offer(E e) //将指定的元素添加为此列表的尾部(最后一个元素)
public boolean offerFirst(E e) //将指定的元素插入此列表的前面。
public boolean offerLast(E e) //将指定的元素插入此列表的末尾。
这些方法的实现大都调用了前文讲述的方法,代码自行阅读即可。
(6)其他方法
public void clear() //从此列表中删除所有元素。
public Object clone() //返回此列表的浅拷贝。(元素本身不会被克隆。)
public Object[] toArray() //以正确的顺序(从第一个元素到最后一个元素)返回包含此列表中所有元素的数组。
看一下toArray()的实现吧,毕竟此方法在平时使用的频率是比较高的:
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}
创建一个数组,并for循环遍历链表对数组进行赋值。
总得来说,LinkedList的源码都是在操作双向链表这个数据结构,阅读源码的难度并不大。