1. LinkedList概述
上大学那会,链表是我们接触最多的数组结构之一,那会我们常用的是单向链表。Java的LinkedList实现了链表这一数据结构,并且实现的是双向链表,LinkedList还提供了双端队列的操作。LinkedList可以存储任何元素,包括null。
和ArrayList不同的是,LinkedList是一个”sequential list”,即LinkedList是一个”顺序存取”的数据结构,而ArrayList是一个”random list”,即ArrayList是一个”随机存取”的数据结构。所谓”seqnential list“,表示该集合只能从前往后或者从后往前遍历存取元素,而不能根据索引立马定位到元素,表现了定位元素的顺序性。相反,”random list”,表示该集合不需要从前往后或者从后往前遍历存取元素,而是可以根据索引立马定位到元素,表现了定位元素的”随机性“。LinkedList继承了AbstractSequentialList正是对”随机存取的体现“。
先来看下LinkedList的类图:
LinkedList实现了List、Deque、Cloneable和Serializable接口,其中Deque是双端队列,因此我们可以将LinkedList当作双端队列使用。
2. 链表节点Node
LinkedList作为一个双端链表,链表节点是通过内部静态类Node实现的,看下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;
}
}
Node的代码很简单,它就是一个双向链表的节点表示。
3. 构造函数
LinkedList有两个构造函数,一个默认构造函数,另一个带有Collection参数的构造函数。
//元素个数
transient int size = 0;
//指向链表第一个节点
transient Node<E> first;
//指向链表最后一个节点
transient Node<E> last;
//默认构造函数
public LinkedList() {
}
//通过另一个集合c来初始化该LinkedList
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
addAll()方法最终调用了另一个重载的方法,看下该重载的方法:
public boolean addAll(int index, Collection<? extends E> c) {
//检查插入的位置
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
//succ是索引index处的节点,pred是该节点的前一节点
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
//将集合c的元素一个一个插入到链表中
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
//链接succ和pred
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
//更新链表元素个数
size += numNew;
modCount++;
return true;
}
该方法将集合c的元素插入到链表位置index开始的位置,很简单,直接看源码注释就可以了。
定位指定索引index处节点的方法是node()方法,看下该方法的实现:
Node<E> node(int 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小于元素个数的一半,从链表的头往尾方向查找,否则从链表的尾往头方向查找。其它的都很简单,遍历指定个数的节点即可。
接着看下其它常用的方法:
4. 常用方法
先看下add方法:
public boolean add(E e) {
linkLast(e);
return true;
}
该方法将指定元素加到链表尾部,调用linkLast将元素链接到链表尾部结点。
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++;
}
linkLast将指定的元素链接到链表的尾部。
继续看下在指定的位置插入元素的方法:
//在指定位置index们插入元素element
public void add(int index, E element) {
//检查插入的位置
checkPositionIndex(index);
//如果在末尾插入,调用linkLast将元素链接到末尾即可
if (index == size)
linkLast(element);
else
//如果不在末尾插入,在index索引之前插入元素element
linkBefore(element, node(index));
}
继续看下linkBefore方法:
//在节点succ之前插入元素e
void linkBefore(E e, Node<E> succ) {
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++;
}
linkBefore方法也很简单,在节点succ前面插入元素e。
继续看下Dequeue相关方法:
//该方法取出队列中第一个元素
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
//该方法返回队列中最后一个元素
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
//删除并返回队列中第一个元素
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
//删除并返回队列中最后一个元素
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
//往队列头添加元素e
public void addFirst(E e) {
linkFirst(e);
}
//往队列尾添加元素e
public void addLast(E e) {
linkLast(e);
}
//返回队头的元素,但是不删除该元素
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//返回队头的元素,并删除该元素
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
这几个方法都很简单,都是链表指针的链接操作,关于队列的操作,以后会专门作介绍,队列也是java一个重要的部分。
总体来说,LinkedList的代码比较简单,本文抽出一些比较有代表性的方法进行了介绍,其他很多方法都是类似的操作,不再说明。