简介
LinkedList 是一个双向链表,他实现了List和Deque(双向队列,既有队列的性质,又有栈的性质)。LinkedList的实现也是非线程安全的,如果有多个线程访问队列,只是读取链表的内容没有关系,当有其中的一个线程修改了list的结构时,在代码中必须保证同步。在链表中添加和删除元素都会引起链表结构的改变,只有修改某个节点的值不会引起结构改变。如果没有保证线程同步的对象,那么可以使用以下代码获取一个线程安全的链表:
List list = Collections.synchronizedList(new LinkedList(...));
其中的迭代器也是一样,一旦创建,只允许调用迭代器的remove或者add方法修改链表的结构,否则就会抛出ConcurrentModificationException
具体结构
-
first代表链表的第一个节点
-
last代表链表的最后一个节点
-
内部节点的结构
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;
}
}
几个重要的函数
下面介绍几个私有函数,这几个函数在增加节点和删除节点时起到调节链表的作用,这几个函数在LinkedHashMap中也有类似函数对应。
- linkFirst
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++; // 节点个数增加1
modCount++; // 结构变化计数 + 1,我更喜欢版本号这个称呼
}
看完上面的代码你会想是不是少修改了一个指针:新节点的指针没有指向原来的第一个节点。
代码中没有忘记,只是在你看不见的地方做了。再看一下在new Node时,传入了一个f,没错,你get到了,在Node的构造函数中修改的指针。
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
这个函数在链表实现队列时,向队列中的头部添加元素时使用。在作为栈使用时,实现压栈的操作。
- linkLast
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; // 否则就将l.next指向新增的节点
size++;
modCount++;
}
这个函数实现了向双向链表的尾部添加一个元素,链表的add方法和队列向尾部添加元素时调用。
- linkBefore
在succ节点之前插入节点e 涉及到的数据结构就是在双向链表的某个节点前插入一个节点,他会在add(index,element) 向链表中的第index位置插入一个元素时使用。
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev; // 缓存 succ的前一个节点
final Node<E> newNode = new Node<>(pred, e, succ); // 新建一个节点,前是succ的前,后是succ
succ.prev = newNode; succ 向前的指针指向新节点
if (pred == null)
first = newNode; 如果 缓存的succ节点为空,说明succ为first节点,现在将 新增的节点改为first节点
else
pred.next = newNode; 否则缓存节点的下一个节点指向新增的节点
size++;
modCount++; //版本号 + 1
}
- unLinkFirst
移除链表的第一个元素,在实现栈的poll时会调用该方法。
/**
* Unlinks non-null first node 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 // 释放引用使GC易于回收(可达性分析)
first = next; // 将第一个指针指向 next
if (next == null)
last = null; //如果之前的第二个节点为空,证明这时候已经将链表移空,最后一个节点指向空
else
next.prev = null; // 否则下一个节点向前的指针指向空
size--;
modCount++;
return element; // 返回第一个节点存储的值
}
- unlinkLast
移除链表的最后一个元素,在作为队列使用时,移除队列中目前最早进入队列的元素
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;
}
- unlink
从链表中移除某个节点,在移除链表中指定位置或指定值的节点时使用。
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; // 前一个节点的next指向后一个节点
x.prev = null; // help GC
}
if (next == null) { // 后一个节点为空,说明移除的是链表的最后一个节点
last = prev; // 最后一个节点指向 前一个节点
} else {
next.prev = prev; // 否则将后一个节点的prev指向前一个节点
x.next = null; // help GC
}
x.item = null; // help GC
size--;
modCount++;
return element;
}
- getFirst getLast
因为LinkedList实现了Deque双向队列,这也是他内部是双向链表的原因
所以他内部就会有这样两个函数
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
上面的两个函数获取链表的第一个或最后一个数据,如果链表为空时会抛出异常。
- removeFirst removeLast addFirst addLast
这四个函数的存在都是为了实现Deque接口,其内部将重复的代码抽出去,形成开篇讲的四个私有函数。
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f); // 从链表中移除第一个节点
}
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l); // 从链表中移除最后一个节点
}
public void addFirst(E e) {
linkFirst(e);
}
public void addLast(E e) {
linkLast(e);
}
- indexOf
查找某个元素的索引,从链表的第一个元素开始查找,contain的内部实现就是调用的这个方法
链表的查找某个节点的时间复杂度是O(n)
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;
}
- add
添加节点直接添加到链表的最后,内部调用的是linkLast(e)
- remove
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; // 返回true
}
}
}
return false;
}
通过indexOf和remove函数可以看出,链表中如果存在多个空值或者相等的值得节点,每次都会移除第一个节点**,这条特性在使用时是很容易犯错的。
- node
返回指定索引的值,从函数可以看出他是假设索引位置是有值的,这个函数只能在同一个包内使用,外面是访问不到的。从效率方面看,减少了角标越界的检查,提高了效率;从单个函数定义来看,这个函数的实现是不安全的,毕竟同一个包内还是可以访问到这个函数的(default);从函数实现上看,这个算法虽然时间复杂度还是为O(n),但是从注释可以看出其还是做了优化的。
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;
}
}
- toArray
public <T> T[] toArray(T[] a) {
if (a.length < size) // 如果传进来的数组大小 小于size 用反射重新创建一个数组
a = (T[])java.lang.reflect.Array.newInstance(
a.getClass().getComponentType(), size);
int i = 0;
Object[] result = a; // 将数据copy到数组
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
if (a.length > size) // 如果数组长度大于size 将数组第size个置空
a[size] = null;
return a;
}
这里重要的是这行代码,
a = (T[])java.lang.reflect.Array.newInstance(
a.getClass().getComponentType(), size);
这行代码根据给定的数组,创建了一个给定数组类型相同的数组,可以指定数组的长度。
总结
LinkedList 内部实现为双向链表,且实现了双向队列的功能,既可以作为队列使用也可以作为栈使用。但是其内部链表的实现结构决定了其在查找链表中某一个元素时的时间复杂度为log(n)。相比于LinkedHashSet,效率低很多。