上一篇分析了ArrayList,ArrayList内部是通过数组实现的,元素在内存中是连续存放的。而LinkedList不同,顾名思义,它的内部是通过链表实现的,准确来说,是双向链表。java里面的容器类,其实就是对各种数据结构进行了封装和实现,所以要分析这些类,其实主要就是分析一下它们各自增删改查的方法都是怎样实现的。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
既然是链表,那么内部肯定存在一个节点类:
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;
}
}
节点内部维护了两根指针,分别指向前驱和后继节点。
有了节点类,就可以开始分析链表了。
1. 成员变量
transient int size = 0; //元素个数
transient Node<E> first; //指向头节点
transient Node<E> last; //指向尾结点
三个成员变量都是瞬时的。
2. 构造器
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
相比于ArrayList,LinkedList的构造器十分简单。
3. 成员方法
成员方法主要就是看增删改查。
- 增
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++; //元素个数+1
modCount++; //修改次数+1
}
- 插入
public void add(int index, E element) {
//检查索引
checkPositionIndex(index);
if (index == size) //索引等于链表长度,直接加在尾部
linkLast(element);
else //否则,先找到索引对应位置的节点,再进行插入
linkBefore(element, node(index));
}
//根据索引查找结点
Node<E> node(int index) {
Node<E> x;
if (index < (size >> 1)) {
//如果索引小于链表长度的一半,就从头开始找
x = first;
for (int i = 0; i < index; i++)
x = x.next;
} else {
//否则,就从尾部开始找
x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
}
return 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 { //节点对象的值不为null
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
//通过索引删除
public E remove(int index) {
//检查索引
checkElementIndex(index);
return unlink(node(index));
}
//移除元素
E unlink(Node<E> x) {
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--; //链表长度-1
modCount++; //修改次数+1
return element; //返回被移除的元素
}
- 改
public E set(int index, E element) {
//检查索引
checkElementIndex(index);
//根据索引查找结点
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
- 查
//查询元素
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
//查询索引
public int indexOf(Object o) {
int index = 0;
if (o == null) { //节点对象的值为null
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else { //节点对象的值不为null
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}
对于增删改查,LinkedList提供了一系列的xxxFirst()
和xxxLast()
方法,来对头和尾进行操作,实现原理也都大同小异,这里就不赘述了。
值得一提的是,LinkedList不仅实现了List接口,还实现了Deque接口
Deque是一个双端队列,实现了Queue接口,在日常的使用中,我们通常是将Deque当作栈或队列来使用,那么在LinkedList中,理所应当的实现了对应的方法。
当我们将LinkedList当作栈来使用时,可以使用以下方法:
public E pop()
出栈public void push(E e)
进栈public E peek()
查看栈顶元素
当我们将LinkedList当作队列来使用时,可以使用以下两组方法:
public boolean add(E e) / public boolean offer(E e)
入队public E remove() / public E poll()
出队public E element() / public E peek()
查看队头元素
这两组方法的区别是:
throw Exception | 返回false或null | |
---|---|---|
入队 | add(E e) | boolean offer(E e) |
出队 | E remove() | E poll() |
查看队头元素 | E element() | E peek() |
4. 特点
了解了LinkedList的内部结构和实现原理,我们能够发现,它有以下特点:
- 按需分配空间,不需要预先分配很多空间。
- 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)。
- 不管列表是否已排序,只要是按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)。
- 在两端添加、删除元素的效率很高,为O(1)。
- 在中间插入、删除元素,要先定位,效率比较低,为O(N),但修改本身的效率很高,效率为O(1)。
可以看出来,LinkedList和ArrayList特点几乎完全相反。但是个人感觉,在日常开发中,如果只是使用List的话,基本上ArrayList就完全够用了。