LinkedList
- 特点
- 允许null值
- 内部以双向链表的形式来保存集合中的元素查询慢,增删快(相比于ArrayList少了数组拷贝)
- 线程不安全
- 所有指定位置的操作都是从头开始遍历进行的
- 素是有序的,输出顺序与输入顺序一致
2.理论与实操同样重要,知其然也要知其所以然
LinkedList查询慢,增删快与ArrayList相反,但同时都是线程不安全的
首先分析其构造方法
//空参构造(使用最多)
public LinkedList() {
}
//有参构造
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);//添加所有集合元素
}
分析方法实现原理源码
1. 添加方法其一
- 原理:
首先看单向链表添加如何实现:
点击这里可以自行操作观察
每次增加一个元素,最后一个元素节点会指向新增加元素(2指向3),我习惯称为前一个元素的后继集节点指向这个元素
同理,在java中,因为是双向链表所以,还有一个节点,我习惯称为元素的前驱节点指向前一个元素(3指向2)
下面探究java中如何实现这一操作
/
public boolean add(E e) {
linkLast(e);//实则调用这个方法,如下
return true;
}
/**
* 先分析: 1、双向链表包括首节点first,尾节点last,元素节点
* 2、每一个元素节点包括三部分:元素、前驱节点、后继节点
* 3、添加元素又分为首次添加、非首次添加
* 4、首次添加:first为空节点,last为空节点,此时创建新元素节点
* 前继节点则为空,后继节点也为空,添加元素后,此元素节点既是first也为last
* 5、非首次添加:创建新元素节点,添加到链表最后,此时应让新元素节点的前驱节点指向当前尾节点(在创建新节点时调用的构造函数中完成)
* 然后新元素变为新的尾节点,因为是双向链表,需要之前尾节点的后继节点同样指向新的尾节点节点完成双向
*/
void linkLast(E e) {
final Node<E> l = last;//此时l代表当前尾节点
/**创建一个节点:l代表尾节点(元素前驱节点)、e当前节点、null代表尾节点(元素后继节点)*/
final Node<E> newNode = new Node<>(l, e, null);//Node方法如下文
//此时让尾节点指向新添加的元素,作为尾节点的标记(上述新的尾节点)
last = newNode;
if (l == null)//在第一次添加元素时,last为空所以l为空,则这个元素就是第一个节点(首节点)
first = newNode;//引出首节点,作为首节点的标记(同时也是尾节点)
else//若不是第一次添加元素,则尾节点last不为空,l则也不为空,
l.next = newNode;//(为了满足双向链表)当前尾节点的后继节点指向新添加的元素节点,至此新添加元素节点变为尾节点(完成双向链接)
size++;//链表元素个数加一
modCount++;//链表操作次数记录加一
}
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;//在创建新节点时,前继节点在这里完成了指向当前尾节点
}
}
2. 添加方法其二
原理:时刻考虑双向链表特性,指定位置添加元素时,改变新增元素前后继节点,以及后元素节点的前指向,和前元素节点的后指向,下图上方为新增节点,在1处新增示意图(大概一个示意图,有点丑),与上述唯一区别就是此方法时在中间添加元素
源码:
//index指定添加元素的位置,element元素值
public void add(int index, E element) {
//判断索引是否越界或者非法(小于零或者大于链表长度)
checkPositionIndex(index);
//若index==size证明需要在链表末尾添加元素(和第一种添加方法实现一致)
if (index == size)
linkLast(element);//同上方法一
else//反之,在指定索引添加元素 首先node(index)找出该索引对应的元素节点
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
// 获取指定位置元素的前驱节点
final Node<E> pred = succ.prev;
//创建新节点,前驱节点指向原指定位置的前继节点,后继结点指向succ节点
//例:1<——2(newNode)——>3
final Node<E> newNode = new Node<>(pred, e, succ);
//上述例子 因为是双向链表所以要同时让2<——3
succ.prev = newNode;
if (pred == null)
//pred 为空证明新添加的元素是第一个节点,那么更新首节点
first = newNode;
else
//反之,也要设置1——>2,保证双向链表
pred.next = newNode;
//元素个数加一
size++;
//记录操作次数
modCount++;
}
其他的添加方法原理基本一样
3. 删除方法
- 原理就是完成下图的整个过程(有了想法才有可能实现!)
指定对象删除
public boolean remove(Object o) {
if (o == null) {//若删除null元素
for (Node<E> x = first; x != null; x = x.next) {//从第一个元素开始,首节点给x,判断条件是x是否有值,有的话进入循环体,然后将x的后继节点付给x(后继节点就相当于下一个元素)
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;
}
指定索引删除
public E remove(int index) {
checkElementIndex(index);//判断索引是否为合法(小于零或者大于链表长度)
return unlink(node(index));//node(index)找到元素节点直接删除如下
}
Node<E> node(int index) {
//根据索引查找元素这里体现了双向链表
if (index < (size >> 1)) {//index小于元素总个数的一半,从前找更快
//从前往后找
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;//顺藤摸瓜找到index位置元素
return x;
} else {//反之从后找更快
//从后往前找
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
发现上述方法就是通过传入的条件找到了要删除的元素最终通过unlink()方法删除下面分析这个方法
E unlink(Node<E> x) {
//x就是要删除的节点,从节点中取出要删除的元素
final E element = x.item;
//取出后继节点
final Node<E> next = x.next;
//取出前驱节点
final Node<E> prev = x.prev;
//下方两个if语句的执行代表了上图过度过程的实现(解链操作)
if (prev == null) {//若前驱节点为null证明,这是第一个元素
first = next;//将要删除的元素节点的下一个元素节点作为第一个元素节点(后继节点指向的元素)
} else {//反之则为中间元素节点
prev.next = next;//上图中过度过程的上方蓝线形成1的后继节点指向3(通过2),同时1指向2红线断裂(被重新赋值覆盖了)
x.prev = null;//断开2的前驱节点(断开了2指向1的那条红线)
}
if (next == null) {//若是最后一个元素
last = prev;//将最后一个元素的前一个元素设置为最后一个元素,记录到尾节点中
} else {
next.prev = prev;//上图中过度过程的下方蓝线形成3的前驱节点指向1(通过2)同时3指向2红线断裂(被重新赋值覆盖了)
x.next = null;//断开2的后继节点(断开了2指向3的那条红线)
}
//最后清空元素,到此元素节点被成功删除
x.item = null;
size--;//元素个数自减
modCount++;//操作次数自增
return element;//返回这个被删除的元素
}
掌握上述方法后再看其他相关删除方法代码,逻辑都一样
4. 查找元素get()方法
public E get(int index) {
checkElementIndex(index);
return node(index).item;//在删除方法中已经说过node方法,得到节点后取出元素返回
}
5. 遍历
- 原理:从第一个元素或者最后一个元素开始遍历,逻辑就是根据得到的第一个元素节点顺藤摸瓜到最后一个元素节点,比较简单不累赘了,(可参考上篇文章arrayList的遍历原理一样,只不过链表是以元素向下寻找,有兴趣可自行跟进源码查看),遍历耗时很慢,每次查找都要从头开始,一般不采用。
好了LinkedList讨论到此结束,下篇文章讨论List集合下的Vector,每天进步一点点!!大家一起讨论学习,不足的地方还请指出来!