一、LinkedList
的继承类图
- 这里可以看到
LinkedList
实现了Deque
接口,也实现了List
接口,说明它既可以作为一个顺序容器,也可以作为一个队列使用。 LinkedList
实现Cloneable
,可以被克隆。LinkedList
实现Serializable
,可以被序列化
二、LinkedList
的成员变量
//队列长度
transient int size = 0;
//头节点
transient Node<E> first;
//尾节点
transient Node<E> last;
- 这里我们发现它有两个成员变量,分别是头节点和尾节点,然后类型是
Node
,我们看下Node
类,这里node
会有指向前一个指针的prev
和指向后一个节点的next
,这样通过这个node
就实现了一个双向链表
private static class Node<E> {
//节点元素内容
E item;
//指向下一个节点
Node<E> next;
//指向前一个节点
Node<E> prev;
三、LinkedList的构造方法
//无参构造器,所有节点默认为null
public LinkedList() {
}
// 集合参数构造器
public LinkedList(Collection<? extends E> c) {
// 调用无参构造器,先创建list
this();
// 调用批量添加
addAll(c);
}
四、常用方法
add(E e)
方法
这里直接调用
addLast
()方法添加到末尾
public boolean add(E e) {
//这里add默认是添加到末尾
linkLast(e);
return true;
}
linkLast
方法
这里首先就是获取当前的
last
节点,然后创建一个新节点,然后把新节点插入到last
节点的末尾,然后判断下如果之前没有last
节点,说明是初始化逻辑,那么这种情况first
节点也应该是他。
void linkLast(E e) {
//获取当前的last节点
final Node<E> l = last;
//创建一个新节点,他的prev节点指向当前的last节点,next节点为空
//如果是初始化的时候,那么他的prev也是null
final Node<E> newNode = new Node<>(l, e, null);
//让新节点成为last节点
last = newNode;
//如果之前没有last节点,说明当前是第一个元素,那么first节点也是他
if (l == null)
first = newNode;
else
//如果之前存在last节点,那么让之前的last节点的next指向新创建的节点
l.next = newNode;
//个数++
size++;
//修改次数++
modCount++;
}
add(int index, E element)
方法
这里首先校验
index
是否合法,然后判断如果index
等于size
说明要插到末尾,那么直接调用last
方法,如果不等于size
,那么获取到当前index
所在的位置,因为当前元素需要放到index
的位置,那么直接放到之前处于index
位置的前面即可。
public void add(int index, E element) {
//检查index是否越界
checkPositionIndex(index);
//如果下标等于size,说明要插入到末尾
if (index == size)
//链接到最后
linkLast(element);
else
//如果下标不等于size,那么首先调用node(index)方法获取到当前对应索引的元素
//然后调用linkBefore插入到指定index位置的前面
linkBefore(element, node(index));
}
node(index)
方法
找到
index
位置的node
,这里做了个优化,判断index
是在前半部分还是在后半部分,然后只遍历一半区域寻找index
即可。
Node<E> node(int index) {
// assert isElementIndex(index);
//这里做个优化,直接二分了,判断是在前半部分区域还是在后半部分区域
//前半部分区域
if (index < (size >> 1)) {
//从first开始往后遍历找index下标的node
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
//从last开始往前遍历找index下标的node
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
linkBefore
方法
插入到指定节点的前面,这个其实就是首先获取到指定节点的前一个节点,然后创建一个新的
node
,然后让指定节点的prev
指向newNode
,如果指定节点的前一个节点为空,说明他是头节点,那么把first
也设置为newNode
,如果不是头节点,那么让前一个节点的next
指向node
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
//在节点succ前拆入节点e,这里没有对succ做非空判断
//获取succ的前一个节点
final Node<E> pred = succ.prev;
//对元素e创建一个新的Node节点
final Node<E> newNode = new Node<>(pred, e, succ);
//让succ的prev指向新Node节点
succ.prev = newNode;
//如果succ的前一个节点为空,说明succ是头节点,那么e要插入在他的前面,e就变成了头节点
if (pred == null)
first = newNode;
else
//如果succ不是头节点,那么让succ的前一个节点的next指向新node节点
pred.next = newNode;
//个数++
size++;
//修改次数++
modCount++;
}
addFirst
方法
调用
linkFirst
方法,链接到头节点
public void aaddFirst(E e) {
linkFirst(e);
}
linkFirst
方法
创建一个新节点,然后让头节点指向它,判断之前是头节点的那个节点,如果它为空的话,说明之前链表中没有节点,那么
last
节点也应该指向它,如果不为空的话,那么需要把它的prev设置为newNode
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
//如果之前有节点,那么让当前节点的prev指向新添加的节点
f.prev = newNode;
//个数++
size++;
//被修改的次数也++
modCount++;
}
remove(Object o)
方法
这里可以发现可以直接允许节点元素为
null
,删除的话也是遍历找到指定节点,然后调用unlink
方法进行删除
public boolean remove(Object o) {
//这里删除也是遍历找到指定节点,并调用unlink删除
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;
}
unlink
方法
这里其实就是断开这个节点和其他节点的关系,判断了几种可能,如果他的
prev
为空,说明他是头节点,那么prev
不需要做改变,如果prev
不为空,那么说明他前面有节点,那么让他的前一个节点的next
指向它的后一个节点,并把当前节点的prev
设置为空,这样就和他前一个节点的关系解除,并把前一个节点和他的下一个节点关联起来了。next
处理方法也是一致。
E unlink(Node<E> x) {
// assert x != null;
//获取当前节点的value,用于最后返回
final E element = x.item;
//获取当前节点的下一个
final Node<E> next = x.next;
//获取当前节点的前一个
final Node<E> prev = x.prev;
//如果前一个节点为空,说明当前节点就是头节点,那么删除当前节点只需要让first指向当前的下一个
if (prev == null) {
first = next;
} else {
//如果当前节点不是头节点,那么让当前节点的前一个节点指向当前节点的下一个节点
prev.next = next;
//让当前节点的prev置为空
x.prev = null;
}
//如果next节点是空,说明删除的节点是尾节点,直接让last指向当前节点的prev即可
if (next == null) {
last = prev;
} else {
//如果next节点不为空,那么让next节点的prev指向当前节点的prev
next.prev = prev;
//让当前节点的next设置为空
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
remove(int index)
方法
这里和上面类似,也是调用
unlink
解除引用关系,只不过他首先要通过node(index)
找到index
下标的node
,这两个方法上面都有解释。
checkElementIndex(index);
return unlink(node(index));
总结
对于随机访问,查询读取操作,ArrayList
优于LinkedList
,因为LinkedList
需要移动指针,而ArrayList
可以直接根据下标进行检索。
大量的增删操作使用LinkedList
,因为ArrayList
需要创建复制数组。(这种也需要看数据量,其实大部分场景我们直接使用ArrayList
就可以了)
平时 coding 中,使用完的对象要置空,帮助GC
回收。(因为我们gc
的时候会进行可达性分析算法,这样的话如果对象还有引用指向的话,就会导致内存泄漏)