以前听同学说LinkedList是循环双向链表,感觉总是很惊奇,头节点Node的prev明明是null,并没有指向尾节点,至少从1.7版本开始LinkedList是直线链表,google了一下,Oracle从1.7开始,对LinkedList做了优化,将环形结构变成了链表结构。所以我们还是应该时刻关注版本的变更。虽然现在java8停止了维护,但是大部分公司还没有应用java9或者刚出的java10,所以我们还是应该在理解java8的基础上再去看java9的9大新特性或者java10的109个新特性。
LinkedList
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
LinkedList继承了AbstractSequetialList,实现了List,可以被用来当作栈,队列。
LinkedList实现了Deque,即LinkedList是一个双向链表。
LinkedList实现了Cloneable,即链表能被克隆。
LinkedList实现了Serialzable,实现了序列化接口,代表LinkedList支持序列化。
LinkedList相对于ArrayList而言,它并不需要扩容,理论上可以无限添加元素。
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;
}
}
transient int size = 0; // 当前节点的个数
transient Node<E> first; //第一个节点
transient Node<E> last; //最后一个节点
每个元素都会被封装成Node节点,每个节点都存储了上一个节点和下一个节点的引用。
LinkedList构造方法
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
public boolean addAll(Collection<? extends E> c) {
return addAll(size, 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;
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
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 = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
默认是一个无参构造,另一个构造方法是传入一个Collection对象,通过this调用默认的构造方法, 接着调用addAll方法,传入size(size == 0),Collection对象,判断index(index>=0 && index<= size)是否越界,将Collection对象转化成Object数组,数组==null,直接返回false。
情况1:根据index=size=0,当此时链表为空的情况下,pred=last,last==null,遍历数组,把数组的每个元素封装成一个Node节点,按顺序连接,把第一个节点设为头节点,将当前链表上的最后一个节点赋值给last。
情况2:当链表不为空的情况下,调用node方法返回指定索引位置上的对象,遍历数组,把 Node节点按照顺序插入到index索引的位置处,然后再把index位置的节点拼接到链表的末尾。
最后修改size的大小,直接返回true。
add方法
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++;
}
首先把要添加的元素封装成一个Node节点,然后Node节点赋给尾节点,如果当前尾节点last为空,说明链表为空,则把Node赋给头节点,这样first=tail=Node;如果尾节点不为空,就把Node节点作为尾节点的下一个节点,Node自然就变成了尾节点,size++,返回true。
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++;
}
在链表的头部添加一个节点作为头节点,那头节点前节点必须为空,所有new Node(null,e,f),first指向当前链表的头节点,如果头节点为空,那么新节点就是链表的第一个节点,first=tail=新节点;如果头节点不为空,新节点就作为first的前节点,这样新节点就变成了头节点。
public void addLast(E e) {
linkLast(e);
}
addlast方法和add方法原理一样,都是把元素追加到链表的末尾。
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
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)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
在index(index >= size && index <= size)没有越界的情况下,如果index=size,说明链表为空,直接使用linklast方法添加元素,否则,查找index位置上的节点succ,以要添加的元素新建节点Node,Node前节点指向succ的前继,后节点指向succ,把Node作为succ的前继,如果succ的前继为空,说明链表为空,Node就是头节点,如果succ前继不为空,Node就是succ的前继的后节点,最后由Node.prev→Node变成了Node.prev→newNode→Node。
get方法
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(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;
}
}
上面的add方法中就用到了node方法,查找的时候采用了二分思想,如果index<链表长度的一半时,从前往后遍历链表;如果index>=链表长度的一半时,从后往前遍历链表,直到index。这样的好处就是不管链表有多长,只遍历链表的一半就可以查找到元素。
set方法
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
查找指定位置上的节点,用新值替换原节点的旧值,并返回该旧值。
remove方法
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
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 = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
remove方法的实质还是调用removefirst方法,即删除链表的第一个节点。如果当前链表为空,直接抛出异常。把链表头节点置为空,把头节点的下一个节点作为头节点,size--,返回被移除的元素。
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
移除链表的最后一个元素,把链尾节点置为空,把链尾的前节点设为尾节点,若前节点为空,则移除之后全部置为空,size--,返回被移除的元素。
public boolean remove(Object o) {
if (o == 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;
}
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;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
移除一个被指定的元素,判断元素是否为空,若是null,遍历链表,把值为null的节点从链表中删除(找到被删除节点的前继和后驱,若前继为空,把后继设为头节点;若前继和后继都是null,则first=last=null;若后继为空,那把前继设为尾节点,若前继和后继都都不为空,前继的下一个节点变为后继,size--),直接返回true,非空操作相同,如果没有找到被移除的元素直接返回false。
clear方法
public void clear() {
// 把链表中的每个节点都置为空,size=0
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}
LinkedList可以当作栈和队列来使用,提供了push,pop,peek,pull,offer等方法,这些方法本质还是调用上面分析的方法,LinkedList没有什么大的变动。