先说特性,再说源码(有这特性的原因),后说应用场合。
先说特性(顺带讲讲ArrayList和LinkedList的区别)
- 双向链表实现,易于增加和删除。链表头尾都有指针,增加和删除只需要改变指针的指向即可。而ArrayList是数组实现,适合遍历和随机查询元素。
- 遍历的效率低。链表相邻结点不在相邻的存储空间(内存不连续)。 而ArrayList是内存连续的,因为第一次扩容的时候一次性申请了整个数组对象的空间,遍历相邻元素不需要再次寻址,因此ArrayList的遍历速度快。
- 不支持随机读取。查询的效率是不稳定的,查询速度最快的是头尾结点,查询最慢的是中间部分的结点(因为要从头 / 尾结点遍历,加上内存不连续)。而ArrayList支持随机读取。
- 允许所有元素,包括null。这点和ArrayList一样。
- 线程不安全,与ArrayList相同,要线程安全可用
Collections.synchronizedList()创建
。多线程并发修改会导致抛出异常。 - 有序集合,输入顺序可与输出顺序相同,毕竟也实现了List接口。
- 相同的元素个数,LinkedList的内存占用应该比ArrayList大,下文说到一个结点Node除了数据item以外还有两个指针,即每个结点需要更多的空间。
再说源码
LinedList实现了Deque、Queue、List 接口,可用于构造 栈、队列、有序集合(序列)。
一、成员变量
//一开始是个空链表
transient int size = 0;
//头结点
transient Node<E> first;
//尾结点
transient Node<E> last;
其中,链表的结点是个双向结点,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结点有 prev指针、 item(数据)、next指针。
LinkedList易于增加和删除的原因:凭借结点的prev(指向前驱结点)和next(指向下一个结点),只需要修改这两个指针的指向就能轻松地在链表头部和尾部增加删除结点。
关于构造函数,只有一个默认构造,就跳过吧。
三、add:把元素放到链表末尾
add(E e)
:链表尾部添加一个元素addAll(int index, Collection<? extends E> c)
:链表指定位置添加一个集合
add(E 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++;
}
需要注意的是:链表没有元素的时候,头结点first和尾结点last都是null,所以判断if (l == null) first = newNode;
是说当链表新增第一个元素时,头结点first和尾结点last都是新元素newNode。
如果链表不空,就让原链表的尾结点last的next指针指向新元素,这时新元素就成为了链表的最后一个元素了。
addAll(int index, Collection<? extends E> c):在某个位置添加一个集合的内容
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
//创建两个节点pred、succ,分别指向要插入位置的前面和后面的节点
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
//遍历要添加内容的数组
for (Object o : a) {
E e = (E) o;
//创建新节点,头指针指向 pred
Node<E> newNode = new Node<>(pred, e, null);
//如果 pred 为null ,说明新建的这个是头节点
if (pred == null)
first = newNode;
else
//pred 指向新建的节点
pred.next = newNode;
//pred 后移一位
pred = newNode;
}
//添加完后需要修改尾指针 last
if (succ == null) {
//如果 succ 为null,说明要插入的位置就是尾部,现在 pred 已经到最后一个元素了
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
//统计元素个数
size += numNew;
modCount++;
return true;
}
具体流程如下图:
图中左边是插入前的状态,右边是插入的过程以及插入完成时的处理。
四、remove()
有多种删除结点的API
E remove(int index)
删除指定位置结点。boolean remove(Object o)
删除某个元素removeFirst()
:删除链表头。removeLast()
:删除链表尾。
remove(int index):删除指定位置结点
public E remove(int index) {
//检查index是否越界
checkElementIndex(index);
return unlink(node(index));
}
E unlink(Node<E> x) {
final E element = x.item;
//获取待删除节点后面、前面的节点next、prev
final Node<E> next = x.next;
final Node<E> prev = x.prev;
//如果前面没有节点,说明 x 是第一个
if (prev == null) {
first = next;
} else {
//前面有节点,让前面节点跨过 x 直接指向 x 后面的节点
prev.next = next;
x.prev = null;
}
//如果后面没有节点,说 x 是最后一个节点
if (next == null) {
last = prev;
} else {
//后面有节点,让后面的节点指向 x 前面的
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
unLink要先判断待删除结点X是否在链表头部或尾部:
- 若X在头部
if (prev == null) first = next;
,让X的下一个结点成为新的头结点first 。 - 若X在尾部
if (next == null) last = prev;
,让X的前一个结点成为新的尾结点last。 - 在其他位置见下图。
remove(Object o):删除指定元素
链表删除指定元素Object o要遍历+equals()比较,如果匹配再调用刚才的E unlink(Node<E> x)
。
public boolean remove(Object o) {
if (o == null) {
//遍历终止条件,不等于 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;
}
removeFirst():删除链表头并返回删除的元素
public E removeFirst() {
final Node<E> f = first;
//链表为空,抛异常
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// 获取数据,作为返回值
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
//现在头节点后边的节点变成第一个了
first = next;
//如果头节点后面的节点为 null,说明原链表只有一个结点,删除之后链表为空,那得设置first =null and last =null
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
判断链表是否为空的标志:last = null 或者 first=null
,哪怕链表有一个结点,first和last都不会为null。如果链表有多个元素,删除头结点后会让头结点first= 原头结点的下一个结点。
五、查询方法
列举了2个常用的查询API
- 获取指定位置的元素:
get(int index)
- 获取元素的下标:
indexOf(Object o)
get(int index):获取指定位置的元素
// 1. 获取指定位置的元素get(int index):要遍历
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
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在靠近头结点
index < (size >> 1)
,就从头结点开始遍历 - 若index在靠近尾结点
index > (size >> 1)
,就从尾结点开始遍历。
int indexOf(Object o):获取元素的第一次出现的下标
public int indexOf(Object o) {
int index = 0;
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
LinkedList也允许所有元素,包括null。从头结点开始遍历,找第一个出现的元素的下标。
六、栈和队列的操作
栈的操作实现(后进先出)
主要在LinkedList的头部操作,涉及到peek()、 push(E e)、pop()
//获取第一个(栈顶),但不删除元素
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//入栈,LinkedList在头部添加元素
public void push(E e) {
addFirst(e);
}
//出栈,LinkedList删除头部元素,具体操作是在上文的removeFirst的unlinkFirst()方法里
public E pop() {
return removeFirst();
}
队列的实现(先进先出)
可以选择在LinkedList 尾部进,头部出,涉及到的方法有element()获得队列头、offer(e)插入队列尾、peek()获得队列头、remove()删除队列头
//获得队列头
public E element() {
return getFirst();
}
//在尾部添加
public boolean offer(E e) {
return add(e);
}
//移除并返回队列头部
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//移除队列头部
public E remove() {
return removeFirst();
}
队列的实现有些不同,存在两个获取队列头部的方法:element() 、peek()
,它们的区别是:
- element()如果队列为空,会抛异常,跟getFirst()方法一致
- peek()如果队列为空,则返回null
后说应用
- 阻塞队列LinkedBlockingQueue应该是用的这个,蛮合适用于生产者消费者模式的。
- 撤销和回滚