LinkedList源码分析总结
前言
本文基于JDK8,如有谬误请各位大佬指正。
概述
-
LinkedList是一个有序集合,底层结构是一个
双向链表
,它允许元素为null -
增删
节点只需要修改节点指针,效率很高;而查改
都需要遍历链表,效率较低 -
线程不安全。使用迭代器遍历时如果进行add,remove等增删元素的操作,就会抛出ConcurrentModificationException。
如须在多线程下使用,可以通过
Collections.synchronizedList(List<T> list)
方法将其转化为线程安全的列表,或直接使用ConcurrentLinkedQueue
类- **Collections.synchronizedList(List list)**的原理其实就是创建了一个SynchronizedList类,其中有一个成员变量mutex作为锁对象,它就是传入列表的class类对象,其中的所有方法都被synchronized关键字来修饰。
- ConcurrentLinkedQueue底层是一个单链表,只有在方法内涉及增删等操作时,才会调用unsafe类中的方法通过
CAS
操作来解决并发问题。(关于CAS问题,这里不再赘述,感兴趣的同学可以自行百度)
源码分析
LinkedList中,我们经常使用的API还是增删查改等操作,下面我们来看看这些方法。
1.成员变量
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
//链表中元素数量
transient int size = 0;
//头结点指针
transient Node<E> first;
//尾结点指针
transient Node<E> last;
}
LinkedList
- 是AbstractSequentialList的子类,实现了List接口,表明它是一个有序集合。
- 实现了Deque接口,而Deque是Queue的子接口,所以LinkedList具有双端增删元素的能力,可以作为队列 和 双向队列的实现类使用
- 实现了Cloneable接口,具有复制对象的能力。Object的native方法clone() 会复制一个对象,它和原对象的内存地址是不同的
- 实现了java.io.Serializable序列化接口,具有序列化和反序列化的能力
2.构造方法
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
3.增
add(E 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++;
modCount++; //修改次数+1
}
内部的Node节点类
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;
}
}
add(int index, E element)
在指定位置index插入一个元素
步骤:
- 对index进行越界检查
- 如果index等于当前链表节点个数(也就是说插入位置为链表尾结点之后),就将它插入表尾。
- 否则遍历链表,将它插入链表指定位置
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
//通过遍历链表找到index位置的节点,遍历前做了优化来提升效率:
//如果index在链表前半部分,就从前往后变量;否则从后往前遍历。
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;
}
}
void linkBefore(E e, Node<E> succ) { //succ为原来index位置的节点
// 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++; //修改次数+1
}
addAll(Collection<? extends E> c)
向链表中加入集合c中所有的元素。插入链表后,这些节点在链表中的排列顺序就是迭代集合c的顺序。
步骤:
- 对插入位置index进行越界检查
- 将集合转化为Object数组
- 通过循环将Object数组中的元素全部插入链表中
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
//向指定位置index
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index); //index越界检查
Object[] a = c.toArray(); //先将集合c转为Object数组
int numNew = a.length;
if (numNew == 0) //集合为空直接返回false,表示添加元素失败
return false;
Node<E> pred, succ;
//先确定index位置的节点succ和它前一个节点pred
if (index == size) { //如果插入链表表尾节点后一个位置
succ = null;
pred = last;
} else { //插入链表表头或中间
succ = node(index); //node方法上面说过,通过优化的遍历来找到index位置的元素
pred = succ.prev;
}
//遍历集合c转换成的数组a
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) { //说明集合c中元素是append到链表表尾的,只需要修改last成员变量
last = pred;
} else { //说明集合c中元素是插入到链表中间的,需要修改下面两个节点的指针
pred.next = succ;
succ.prev = pred;
}
size += numNew; //当前节点个数 = 原链表节点个数+插入的集合中元素个数
modCount++; //修改次数+1
return true;
}
4.删
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(int index)
移除链表指定位置的节点
步骤:
- 检查index是否越界
- 获取index位置的节点
- 修改指针域将index位置的节点删除
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
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;
//移除index位置节点x的前驱引用
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;
}
remove(Object o)
通过遍历链表,移除第一个和指定元素o相等的节点
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;
}
5.改
set(int index, E element)
public E set(int index, E element) {
checkElementIndex(index); //越界检查
Node<E> x = node(index); //遍历获取index位置节点
E oldVal = x.item;
x.item = element; //修改该节点存储内容
return oldVal;
}
6.查
get(int index)
移除指定位置的元素。
public E get(int index) {
checkElementIndex(index); //越界检查
return node(index).item; //遍历获取
}
toArray方法
再看看toArray的两个构造方法。
两个方法非常类似,都是先创建一个size大小的数组,再通过遍历链表将每个节点存储的内容赋给数组,只不过下面的那个是通过反射创建数组而已。
public Object[] toArray() {
Object[] result = new Object[size];
int i = 0;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
return result;
}
public <T> T[] toArray(T[] a) {
if (a.length < size)
a = (T[])java.lang.reflect.Array.newInstance(
a.getClass().getComponentType(), size);
int i = 0;
Object[] result = a;
for (Node<E> x = first; x != null; x = x.next)
result[i++] = x.item;
if (a.length > size)
a[size] = null;
return a;
}