一、LinkedList概述
1.初识LinkedList
LinkedList是基于链表实现的(双向链表)
什么是链表?
链表原先是C/C++的概念,是一种线性的存储结构,意思是将要存储的数据存在一个存储单元里面,这个存储单元里面除了存放有待存储的数据以外,还存储有其下一个存储单元的地址(下一个存储单元的地址是必要的,有些存储结构还存放有其前一个存储单元的地址),每次查找数据的时候,通过某个存储单元中的下一个存储单元的地址寻找其后面的那个存储单元。
1、链表中任意一个存储单元都可以通过向前或者向后寻址的方式获取到其前一个存储单元和其后一个存储单元
2、链表的尾节点的后一个节点是链表的头结点,链表的头结点的前一个节点是链表的尾节点
2.LinkedList数据结构原理
每个节点除含有元素外,还包含向前,向后的指针。 新建一个LinkedList,生成一个头节点(header,就是一个头指针),它的元素为null。
它自包含,next和previous指针都指向自己。 执行add(Object obj)方法后,会生成一个新节点
Header节点的next指向链表的第一个节点,previous指向链表的最后一个节点,在这里都是first。 再增加一个对象,它的形状像下面这样
现在是一个标准的双向链表形状。每个节点都有自己的next和previous指针。
增加节点,只会对链表的指针进行操作,速度快
LinkedList实现了Deque,所以它有双向队列的特征,在链表两端可增删数据
使用index查找对象时,会以index和size/2比较,从前或从后向中间搜索
ListIterator可向前或向后迭代
双向链表必定存在一种数据结构——我们可以称之为节点,节点实例保存业务数据、前一个节点的位置信息和后一个节点位置信息,如下图所示:
3.私有属性
LinkedList中定义了两个私有属性:
private transient Entry<E> header = new Entry<E>(null, null, null);
private transient int size = 0;
元素 | 作用 |
---|---|
header | 双向链表的头节点,它是双向链表节点所对应的类Entry的实例。Entry中包含成员变量: previous, next,element。其中,previous是该节点的上一个节点,next是该节点的下一个节点,element是该节点所包含的值。 |
size | 双向链表中节点实例的个数 |
Entry底层源码:
private static class Entry<E> {
E element;
Entry<E> next;
Entry<E> previous;
Entry(E element, Entry<E> next, Entry<E> previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
LinkedList的Entry中的"E element",就是它真正存储的数据." Entry next"和"Entry previous"表示的就是这个存储单元的前一个存储单元的引用地址和后一个存储单元的引用地址。用图表示就是:
4.构造函数
LinkedList提供了两个构造方法:
public LinkedList() {header.next = header.previous = header;} | public LinkedList(Collection<? extends E> c) { this();addAll©;} |
---|---|
不接受参数,将header实例的previous和next全部指向header实例(注意,这个是一个双向循环链表,如果不是循环链表,空链表的情况应该是header节点的前一节点和后一节点均为null),这样整个链表其实就只有header一个节点,用于表示一个空的链表。执行完构造函数后,header实例自身形成一个闭环,例图:![]() | 接收一个Collection参数c,调用第一个构造方法构造一个空的链表,之后通过addAll将c中的元素全部添加到链表中。 |
5.LinkedList四大关注点
关注点 | 结论 |
---|---|
是否允许为空 | 允许 |
是否允许有重复数据 | 允许 |
是否有序 | 是(按照增加顺序排列) |
线程是否安全 | 线程不安全 |
二、添加元素
添加元素代码示例:
public static void main(String args[]){
List<String> list = new LinkedList<String>();
list.add("张三");
list.add("李四");
}
从LinkedList源码分析三行代码是如何执行的:
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
//new一个Entry出来,全为null
private transient Entry<E> header = new Entry<E>(null, null, null);
private transient int size = 0;
/**
* Constructs an empty list.
*/
//执行构造函数,设置引用地址
public LinkedList() {
header.next = header.previous = header;
}
...
}
new了一个Entry出来名为header,Entry里面的previous、element、next都为null,执行构造函数的时候,将previous和next的值都设置为header的引用地址
再看 list.add(Object o) 方法源码:
public boolean add(E e) {
addBefore(e, header);
return true;
}
private Entry<E> addBefore(E e, Entry<E> entry) {
Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
size++;
modCount++;
return newEntry;
}
addBefore(E e,Entry entry)方法是个私有方法,所以无法在外部程序中调用(也可通过反射调用)
addBefore(E e,Entry entry)先通过Entry的构造方法创建e的节点newEntry(包含了将其下一个节点设置为entry,上一个节点设置为entry.previous的操作,相当于修改newEntry的“指针”),之后修改插入位置后newEntry的前一节点的next引用和后一节点的previous引用,使链表节点间的引用关系保持正确。之后修改和size大小和记录modCount,然后返回新插入的节点。
new了一个Entry出来,可能不太好理解,根据Entry的构造函数,我把这句话"翻译"一下,可能就好理解了:
1、newEntry.element = e;
2、newEntry.next = header;
3、newEntry.previous = header.previous;
header的引用地址为0x00000000,header.previous上图中已经看到了,也是0x00000000,那么假设new出来的这个Entry的地址是0x00000001,继续画图表示:
一共五步,每一步的操作步骤都用数字表示出来了:
1、新的entry的element赋值为111;
2、新的entry的next是header的引用地址,header的引用地址是0x00000000,所以新的entry的next即0x00000000;
3、新的entry的previous是header的previous,header的previous是0x00000000,所以新的entry的next即0x00000000;
4、“newEntry.previous.next = newEntry”,首先是newEntry的previous,由于newEntry的previous为0x00000000,所以newEntry.previous表示的是header,header的next为newEntry,即header的next为0x00000001;
5、“newEntry.next.previous = newEntry”,和4一样,把header的previous设置为0x00000001;
为什么要这么做?还记得双向链表的两个特点吗,一是任意节点都可以向前和向后寻址,二是整个链表头的previous表示的是链表的尾Entry,链表尾的next表示的是链表的头Entry。现在链表头就是0x00000000这个Entry,链表尾就是0x00000001,可以自己看图观察、思考一下是否符合这两个条件。
最后看一下add了一个字符串"222"做了什么,假设新new出来的Entry的地址是0x00000002,画图表示:
还是执行的那5步,图中每一步都标注出来了,只要想清楚previous、next各自表示的是哪个节点就不会出问题了。
至此,往一个LinkedList里面添加一个字符串"111"和一个字符串"222"就完成了。从这张图中应该理解双向链表比较容易:
1、中间的那个Entry,previous的值为0x00000000,即header;next的值为0x00000002,即tail,这就是任意一个Entry既可以向前查找Entry,也可以向后查找Entry。
2、头Entry的previous的值为0x00000002,即tail,这就是双向链表中头Entry的previous指向的是尾Entry。
3、尾Entry的next的值为0x00000000,即header,这就是双向链表中尾Entry的next指向的是头Entry。
三、查找元素
查找元素源码分析:
public E get(int index) {
return entry(index).element;
}
// 获取双向链表中指定位置的节点
private Entry<E> entry(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+size);
Entry<E> e = header;
// 获取index处的节点。
// 若index < 双向链表长度的1/2,则从前向后查找;
// 否则,从后向前查找。
if (index < (size >> 1)) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
get(int)方法首先判断位置信息是否合法(大于等于0,小于当前LinkedList实例的Size),然后遍历到具体位置,获得节点的业务数据(element)并返回。
注意:为了提高效率,需要根据获取的位置判断是从头还是从尾开始遍历。
这段代码就体现出了双向链表的好处了。双向链表增加了一点点的空间消耗(每个Entry里面还要维护它的前置Entry的引用),同时也增加了一定的编程复杂度,却大大提升了效率。
由于LinkedList是双向链表,所以LinkedList既可以向前查找,也可以向后查找,当index小于数组大小的一半的时候(size >> 1表示size / 2,使用移位运算提升代码运行效率),从前向后查找;否则,从后向前查找。(类似于折半查找)
注意:位运算与直接做除法的区别。先将index与长度size的一半比较,如果index<size/2,就只从位置0往后遍历到位置index处,而如果index>size/2,就只从位置size往前遍历到位置index处。这样可以减少一部分不必要的遍历。
四、删除元素
看完了添加元素,我们看一下如何删除一个元素。和ArrayList一样,LinkedList支持按元素删除和按下标删除,前者会删除从头开始匹配的第一个元素。用按下标删除举个例子好了,比方说有这么一段代码:
public static void main(String[] args)
{
List<String> list = new LinkedList<String>();
list.add("张三");
list.add("李四");
//删除"张三"元素
list.remove(0);
}
删除元素源码解析:
public E remove(int index) {
return remove(entry(index));
}
private E remove(Entry<E> e) {
if (e == header)
throw new NoSuchElementException();
// 保留将被移除的节点e的内容
E result = e.element;
// 将前一节点的next引用赋值为e的下一节点
e.previous.next = e.next;
// 将e的下一节点的previous赋值为e的上一节点
e.next.previous = e.previous;
// 上面两条语句的执行已经导致了无法在链表中访问到e节点,而下面解除了e节点对前后节点的引用
e.next = e.previous = null;
// 将被移除的节点的内容设为null
e.element = null;
// 修改size大小
size--;
modCount++;
// 返回移除节点e的内容
return result;
}
删除元素首先得找到元素所在索引,跟get()方法一样
理解:
1.找到删除元素所在位置
2.e.previous.next = e.next;//将删除节点的前一节点的后指针指向删除节点的后一个节点
3.e.next.previous = e.previous;//将删除节点的后一节点的前指针指向删除节点的前一个节点。
4.清空预删除节点:
e.next = e.previous = null;
e.element = null;
交给gc完成资源回收,删除操作结束。
与ArrayList比较而言,LinkedList的删除动作不需要“移动”很多数据,从而效率更高。
这里我提一点,第4步将待删除的Entry的previous、element、next都设置为了null,这三步的作用是让虚拟机可以回收这个Entry
五、LinkedList和ArrayList的对比
老生常谈的问题了,这里我尝试以自己的理解尽量说清楚这个问题,顺便在这里就把LinkedList的优缺点也给讲了。
1、顺序插入速度ArrayList会比较快,因为ArrayList是基于数组实现的,数组是事先new好的,只要往指定位置塞一个数据就好了;LinkedList则不同,每次顺序插入的时候LinkedList将new一个对象出来,如果对象比较大,那么new的时间势必会长一点,再加上一些引用赋值的操作,所以顺序插入LinkedList必然慢于ArrayList
2、基于上一点,因为LinkedList里面不仅维护了待插入的元素,还维护了Entry的前置Entry和后继Entry,如果一个LinkedList中的Entry非常多,那么LinkedList将比ArrayList更耗费一些内存
3、有些说法认为LinkedList做插入和删除更快,这种说法其实是不准确的:
(1)LinkedList做插入、删除的时候,慢在寻址,快在只需要改变前后Entry的引用地址
(2)ArrayList做插入、删除的时候,慢在数组元素的批量copy,快在寻址
所以,如果待插入、删除的元素是在数据结构的前半段尤其是非常靠前的位置的时候,LinkedList的效率将大大快过ArrayList,因为ArrayList将批量copy大量的元素;越往后,对于LinkedList来说,因为它是双向链表,所以在第2个元素后面插入一个数据和在倒数第2个元素后面插入一个元素在效率上基本没有差别,但是ArrayList由于要批量copy的元素越来越少,操作速度必然追上乃至超过LinkedList。
从这个分析看出,如果你十分确定你插入、删除的元素是在前半段,那么就使用LinkedList;如果你十分确定你插入、删除的元素在比较靠后的位置,那么可以考虑使用ArrayList。如果你不能确定你要做的插入、删除是在哪儿呢?那还是建议你使用LinkedList吧,因为一来LinkedList整体插入、删除的执行效率比较稳定,没有ArrayList这种越往后越快的情况;二来插入元素的时候,弄得不好ArrayList就要进行一次扩容,记住,ArrayList底层数组扩容是一个既消耗时间又消耗空间的操作。