LinkedList和ArrayList都是使用频率非常高的列表。
LinkedList底层是一个双向链表的数据结构,它的插入和删除非常的快速,只需要修改对应节点的next
和prev
的指向即可,不像ArrayList需要大量的移动元素的位置。
LinkedList没有下标,不支持快速随机访问,因此没有实现java.util.RandomAccess
接口,遍历列表时应该优先采用迭代器的方式,性能会更好一些,LinkedList用for循环便利是灾难!!!
但是LinkedList实现了java.util.Deque
接口,因此它也是一个双向队列,支持push
、pop
、peek
、pool
等操作。
属性
// 元素的大小
transient int size = 0;
// 指向链头的指针
transient Node<E> first;
// 指向链尾的指针
transient Node<E> last;
构造函数
LinkedList没有所谓的容量一说,它是无界的,只要有内存可用,就能新建节点通过next相连,因此它的构造函数比ArrayList少一些。
/**
* 构造一个空的链表
*/
public LinkedList() {
}
/**
* 给定一个集合,将所有元素都添加到链表中
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
核心操作
LinkedList的源码相比ArrayList要简单,不管是插入还是删除,都不需要移动元素,只需要修改节点的指向即可,添加元素时,也没有扩容一说,直接新建节点通过指针连接即可,代码量要少的多。
add
add(E e)
添加元素到链尾:
- 新建节点newNode
- 前任last的next指向newNode
- newNode的prev指向前任last
- last指向newNode。
// 添加元素到链尾
public boolean add(E e) {
linkLast(e);
return true;
}
/*
将元素插入到链尾。
LinkedList的插入过程还是很简单的,修改节点指向即可,不存在扩容的问题。
*/
void linkLast(E e) {
final Node<E> l = last;
// 创建一个Node节点入链,当前节点的prev指向前任链尾
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
// last为空,代表是第一次插入,则newNode即是链头,也是链尾。
if (l == null)
first = newNode;
else
// 前任链尾的next指向当前节点
l.next = newNode;
size++;
modCount++;
}
add(int index, E element)
将元素插入到指定位置:
- 校验index的合法性
- 如果index==size,直接插入到链尾。
- 否则找到下标index对应的节点,插入到它的前面。
// 将元素插入到指定位置
public void add(int index, E element) {
// 首先是检查index是否合法:index >= 0 && index <= size
checkPositionIndex(index);
// 如果index=size,则直接插入到链尾
if (index == size)
linkLast(element);
else
/*
说明插入的是中间位置:
1.找到第index元素
2.创建一个Node节点,插入到它的前面
*/
linkBefore(element, node(index));
}
private void checkPositionIndex(int index) {
if (!isPositionIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
// 校验index是否合法
private boolean isPositionIndex(int index) {
return index >= 0 && index <= size;
}
node(int index)
通过下标查找对应的节点,这里JDK做了个优化,如果index过半了,则从链尾开始查找,否则从链头开始查找:
/*
找到第index位置的元素
这里做了一个优化:
如果inex没有过半,则从链头开始查找。
如果index过半,则从链尾开始查找。
*/
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对应的节点后,插入到它的前面:
/*
将元素e插入到succ的前面。
1.新建一个Node节点,prev指向succ的prev,next指向succ。
2.succ的prev指向新建节点。
3.succ的prev的next指向新建节点。
*/
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)
// succ的prev是null,说明succ是链头,现任链头指向新建节点
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
addAll(Collection<? extends E> c)
将给定集合内的元素添加到链表中:
// 将集合追加到链尾
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
// 将集合追加到index位置
public boolean addAll(int index, Collection<? extends E> c) {
// 检查index是否合法
checkPositionIndex(index);
// 将集合转数组,为空则返回false
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
Node<E> pred, succ;
if (index == size) {
// 插入到链尾
succ = null;
pred = last;
} else {
// 找到第index位置的元素
succ = node(index);
pred = succ.prev;
}
// 循环新建节点插入
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;
}
if (succ == null) {
// 如果是追加在链尾,则需要将last指向集合的最后一个节点
last = pred;
} else {
/*
如果插入的是中间位置:
1.将集合的最后一个节点的next指向原第index位置的元素。
2.原第index位置的元素的prev指向集合的最后一个元素.
*/
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
addFirst(E e)
由于LinkedList实现了Deque接口,因此它也是一个双向队列,支持从队头添加:
/*
由于LinkedList实现了Deque接口,因此它也是一个双向队列。
添加元素到链头。
*/
public void addFirst(E e) {
linkFirst(e);
}
/*
将e插入到链头:
1.新建节点,next指向原first
2.first指向新建节点
3.原first的prev指向新建节点
*/
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
// 说明原本链表是空的,e是第一个元素,last也要指向它
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
addLast(E e)
既然是双向队列,那肯定也支持队尾添加了,和add()是一样的。
/*
由于LinkedList实现了Deque接口,因此它也是一个双向队列。
添加元素到链尾,和add(E e)一样。
*/
public void addLast(E e) {
linkLast(e);
}
get
LinkedList没有索引,因此顺序访问性能还不错,但是随机访问性能就很差了。
get(int index)
获取第index位置的元素,node()方法代码贴在前面了,过半从链尾开始查找,否则从链头开始查找,最大的查找次数不会超过一半。
/*
获取第index位置的元素:
LinkedList基于链表,没有下标,因此只能:
1.从first开始,通过next从头向尾找。
2.从last开始,通过prev从尾向头找。
*/
public E get(int index) {
// 校验index合法性:index >= 0 && index < size
checkElementIndex(index);
/*
查找第index位置的元素,做了优化:
1.index过半,从链尾向链头查找。
2.index没过半,从链头向链尾查找。
*/
return node(index).item;
}
getFirst()和getLast()
由于也是双向队列,因此支持快速访问队头和队尾元素:
/*
由于LinkedList实现了Deque接口,因此它也是一个双向队列。
获取链头元素。
*/
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
/*
由于LinkedList实现了Deque接口,因此它也是一个双向队列。
获取链尾元素。
*/
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
indexOf
indexOf(Object o)
从链头开始查找,获取第一个匹配项的index:
/*
从链头开始查找,获取第一个匹配项的index。
for循环存在重复代码,将o的非空判断外提了,避免重复判断,适当的重复是有必要的。
1.为什么不直接用:x.item.equals(o)???
因为LinkedList允许存入null。
*/
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;
}
remove
remove()
移除链头元素,并返回:
/*
移除链头元素,并返回
*/
public E remove() {
return removeFirst();
}
/*
移除并返回链头元素
*/
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
/*
移除链头元素并返回:
1.first指向原链头的next。
2.新first的prev置空。
*/
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
// 链表只有一个元素,移除后就空了,因此last也要置空。
last = null;
else
// 新first的prev置空
next.prev = null;
size--;
modCount++;
return element;
}
remove(int index)
移除指定位置的元素并返回:
/*
移除指定位置的元素并返回:
1.通过node(index)找到需要移除的元素x。
2.x的next的prev指向x的prev。
3.x的prev的next指向x的next。
*/
public E remove(int index) {
// 检查index的合法性
checkElementIndex(index);
return unlink(node(index));
}
/*
移除元素x:
1.x的next的prev指向x的prev。
2.x的prev的next指向x的next。
*/
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) {
// x是链头,x移除后,next就是链头了。
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
// x是链尾,x移除后,prev就是链尾了。
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;//置空,帮助GC
size--;
modCount++;
return element;
}
peek
peek()和peekFirst()
来自Deque,偷看一眼链头的元素,但不移除:
/*
来自Deque
获取链头元素,但不移除。
*/
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
peekLast()
来自Deque,偷看一眼链尾的元素,但不移除:
/*
来自Deque
获取链尾元素,但不移除。
*/
public E peekLast() {
final Node<E> l = last;
return (l == null) ? null : l.item;
}
poll
来自Deque,获取链头或链尾的元素,并将其移除:
/*
来自Deque
返回链头元素,并将其移除。
*/
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
/*
返回链头元素,并将其移除。
*/
public E pollFirst() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
/*
来自Deque
返回链尾元素,并将其移除。
*/
public E pollLast() {
final Node<E> l = last;
return (l == null) ? null : unlinkLast(l);
}
offer
来自Deque,向链表的链头或链尾处塞入元素,和addFirst
、addLast
一样,代码就不重复贴了。
push
来自Deque,向链尾处添加元素,和addFirst
一样,代码就不重复贴了。
pop
来自Deque,弹出链头的元素,调用的还是unlinkFirst
:
/*
移除并返回链头元素
*/
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
总结
LinkedList整体代码还是比较简单的,它的操作没有ArrayList那么复杂,阅读起来比较轻松。
LinkedList它既是一个双向链表,又是一个双端队列,顺序访问效率高,随机访问效率低,如果要对LinkedList进行遍历,应该使用迭代器来遍历,for循环遍历是灾难!!!