集合:LinkedList实现
LinkedList类关系介绍
LinkedList是一类链表结构的集合,插入、删除等操作效率高,查询效率低。LinkedList除了实现List接口,还实现了Deque接口。Deque是Queue的子接口,是扩展Queue接口,即可用于FIFO(First in First out)也可用于LIFO(Last in First out)的结构队列,可在头部和尾部检索、插入、删除。因而LinkedList也具有双向特性和更加灵活丰富的双向操作方法,LinkedList继承实现关系图参考下方。

LinkedList数据结构
LinkedList中有一个内部私有类Node,这个类就是LinkedList数据结构的基础,它代表着链表的每一个节点,并且除了自身节点的值item外,还持有前一个节点和后一个节点,所以说LinkedList是双向链表,即可向前检索,也可以向后检索。
Node结构源码:
/**
* linkedList的底层链表节点数据结构
* @param <E> 集合内存放的对象类型
*/
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中构造函数Node(Node<E> prev, E element, Node<E> next) 用于构建新节点,并关联前一个节点及后一个节点。在插入节点的方法中会用到,会在后面的源码中多次出现。下图是LinkedList的结构图,这里只表示关系结构,不代表内存分配也是该结构。实际上,每一个节点Node的内存分配并不是连续的,而是随机离散的。

transient int size = 0;
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
LinkedList有first和last两个成员变量,分别为头部节点和尾部节点,并且按照注释理解,first、last具有以下关系:
- 当first与last有一方为null时,另一方必定也为null,链表必为空链表。
- first与last不为null前提下,first节点的prev与last节点的next一定为null,两者的item一定不为null。(这里的item一定不为null,代码中并未做强约束,实际上LinkedList是支持添加null。这里额外补充,idea中debug不显示null,只会显示not showing null elements,这里需要在Intellij idea > Preferences > Build,Execution,Deployment > Debugger > Data Views > java 这里取消勾选Hide null elements in arrays and collections。这里我的idea是2018.1 mac版本,其他版本可能会有出入,直接打开Preferences后搜索Debugger,再找类似的位置)
下方图为LinkedList中的方法,展示每个方法实现了哪个接口。

这里我们主要看以下几个方法的源码:
- 实现Deque的addFirst(E)、addLast(E)、removeFirstOccurrence(Object)方法
- 实现AbstractSequentialList的addAll(Collection<? extends E>>)、get(int)、remove(int)方法
- 实现AbstractList的add(E)方法
addFirst(E)
addFirst(E)不是List接口下的方法,所以一般使用List的情况下(List<String> users = new LinkedList<String>()),不常用这个方法,要使用的话还需要做一次类型转换,实现Deque的其他方法同理。
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
addFirst方法实际上调用的是linkFirst(e)方法。这里就是将传入的Element构建成新的节点插入链表的头部,后续判断first是否为null(空链表),若是,将新节点也设置为last,即LinkedList只有一个节点时,first和last均为该节点;若不是,将原先的头部节点的前一个节点赋值为新的头部节点。对于链表的操作,关注两个点,一个就是新节点插入与前后节点的关联,另外就是新节点插入位置的前后节点与新节点关联。
addLast(E) 与 add(E)
两个方法都是调用了linkLast(e),所以主要看这个方法。
public void addLast(E e) {
linkLast(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++;// 集合长度加1
modCount++;// 修改次数加1 用于fast fail(快速失败策略)
}
代码很简单,比较好理解。这里我们可以看到调用add(E),就是在链表的尾部添加新节点。
removeFirstOccurrence(Object)
这里实际调用的是实现AbstractCollection的remove(Object o)方法,而remove(o)实际调用的是unlink(x)方法,所以主要看unlink(x)做了哪些事。
public boolean removeFirstOccurrence(Object o) {
return remove(o);// 调用的是remove方法
}
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);// 主要还是看unlink(x)这个方法
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);// 主要还是看unlink(x)这个方法
return true;
}
}
}
return false;
}
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;// 帮助 GC
}
if (next == null) {// 删除的节点为尾部节点
last = prev;// 将删除的节点的前一个节点作为新的尾部节点
} else {
next.prev = prev;// 建立后节点与前节点的链表关系
x.next = null;// 帮助 GC
}
x.item = null;// 帮助GC
size--;// 集合长度减1
modCount++;// 修改次数加1 用于fast fail(快速失败策略)
return element;// 返回被删除的节点值
}
这里可以看到,源码中,将删除的节点的item、next、prev都设为null,源码的注释为help GC,java虚拟机HotSpot采用的是根搜索算法,即便不做设置为null的操作,Node也是没有任何GC Root可达,依旧会被回收。这里可能考虑到其他垃圾回收算法(如引用计数算法),使删除的节点不与其他对象存在引用关系,便于快速回收。
addAll(Collection<? extends E>>)
实际上调用的是addAll(int index, Collection<? extends E> c)方法
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
// 判断是否越界 仅对addAll(int index, Collection<? extends E> c)
// 因为 size >=0 and size <= size 恒成立
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
// succ表示index位置的节点对象
// pred表示succ的前一个节点对象
Node<E> pred, succ;
// 这里是对 addAll(Collection<? extends E> c) 和 addAll(int index, Collection<? extends E> c) 处理
if (index == size) {// 这里已经将 size为0的情况处理了
// 如果index == size 说明 直接往双向链表的末尾插入新的集合
succ = null;// last 对象的index为 size -1 所以size为null ,直接赋值
pred = last;// pred 即为last节点(当size为0,pred = last = 0)
} else {
succ = node(index);// 将succ设置为index位置的节点
pred = succ.prev;// pred设置为index的前一个节点
// P.S. pred = succ.prev = node(index - 1) 链表通过index查找效率低,找到对应节点后,通过节点持有的上下对象获取会更高效
}
for (Object o : a) {
@SuppressWarnings("unchecked") E e = (E) o;
// 确定好集合插入的位置后,通过Node的带参构造函数,构建节点并关联进链表中
// pred-> 上一个 e->当前持有对象 next-> 下一个
Node<E> newNode = new Node<>(pred, e, null);// next为null 这里可以看出 是往双向链表的末尾插入新的集合节点
if (pred == null)
// 若集合一开始没有节点 将设置进去的第一个节点设为链表的第一个节点
first = newNode;
else
pred.next = newNode;// 有了第一个节点后,此后就将下一个节点设置为当前模拟指针节点的下一个节点
pred = newNode;// 再将模拟指针节点设置为新建的节点,以便循环再往下添加节点
}
if (succ == null) {// size 0 或者 size不为0 在链表末尾插入新节点的场景
last = pred;// 将最后一个添加进入的节点设为last
} else {
// 在初始的pred后添加完节点后,将新pred与原先的succ节点进行关联
pred.next = succ;
succ.prev = pred;
}
size += numNew;// 修改size
modCount++;// 用于集合的快速失败检测机制
return true;// 返回添加成功
}
remove(int) 与 get(int)
remove(int)核心方法还是unlink(E),我们着重看node(index)方法。
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {// 判断index在链表的前半部还是在后半部
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;
}
}
node(int)会先判断index在链表的前半部还是在后半部,size>>1等于size/2
size >> 1 = size / 2 ^ 1
位运算做运算效率高,但是可读性差。如果是一些封装方法并且注重效率,感觉位运算就是个很好的选择。
判断节点位置后,若在前半部则从头部开始检索,反之则从尾部开始。
LinkedList使用注意项
不要使用普通for循环get(int)去遍历LinkedList,由上面的get(int)方法也可以看出,LinkedList需要一个一个节点去查找,若是链表有10个节点,index为5,链表需要从头部节点开始获取next节点,再根据next节点的next节点继续获取下去,直至第5个节点。若是遍历一个长度为10的链表,需要查找1+2+3+4+5+5+4+3+2+1次节点,后半部会从尾端节点开始查找,所以越靠近首尾,需要查找节点的数量会减少。这里我们可以计算遍历LinkedList的时间复杂度:
((1+N/2)*N/2)/2*2 = N/2+N^2/4
可以得出遍历LinkedList的时间复杂度为o(N^2),所以还是建议用Iterator来遍历,无论用for还是while循环迭代器,效率都会远远高于for循环get(int)的方式。LinkedList内部私有类ListItr实现了ListIterator,迭代器是用一个next成员变量,维护了一个类似模拟指针的变量,让集合能快速地获取到下一个节点。

浙公网安备 33010602011771号