可以关注我的微信公众号:xiaobei109208,每周一篇技术分享哦。
“各位周末好呀~”
“啊呸,程序员哪里来的周末”
前两天怀着激动的心,颤抖的手写了数据结构-ArrayList源码解析,之后我也仔细的看了。确实,不可否认的写的有些许粗糙,但是还好文章要表达的中心思想没跑偏,后面的我会更努力写的更专业一点,我也需要点时间,不足之处或技术要点有更好的见解,可直接点名批评,我必定虚心请教,并呈上十二分的敬意,毕竟这是最难能可贵的。
今天!!!就说一说它的姊妹篇LinkedList
LinkedList:为什么不是兄弟篇????
NPC:enmmm………...
最怕空气突然安静,场面一度又尴又尬………...
还是那句话,知其然先知其所以然,深入原理知晓原理,才能在之后合适的时间合适的项目采用合适的方法做着合适的事。
总结:全是废话!!!
LinkedList的前世今生
是的,没错,天也不早了,干正事。
上篇文章顺带说了一下LinedList的底层实现结构,双向链表,那么问题来了,啥玩意叫双向链表?拿图说话:
双向链表中,每个元素都包含三个字段:
item:当前保存的值
prev:指向前一个元素的节点
next:指向后一个元素的节点
(以上是按照jdk1.8的源码来说的,先简单了解什么是双向链表,后面再针对源码讲解方法)
这里先说一说它的过去,其实在jdk1.7之前LinkedList底层实现结构是双向循环链表,那什么又是双向循环链表?就好比……一条项链,相辅相成环环相扣,闪闪惹人爱。在进行添加和删除的时候,只需要改变指针的指向,把链表断开,添加/删除元素,再把链表重新连起来即可。并且在jdk1.7之前底层代码实现也做了调整,LinkedList是通过header Entry实现的一个循环链表,先初始化一个空的Entry,用来做header,然后首尾相连,形成一个循环链表。
jdk 1.6版本源码
private transient Entry<E> header = new Entry<E>(null, null, null);
private transient int size = 0;
定义一个空的Entry对象作为头节点,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构造函数,无参,并且将header节点的前后结点都设置为其本身,这样整个链表其实就只有一个header节点。
注意:双向循环链表和双向链表的代码区别就在这里,如果不是循环链表,空链表的情况应该是header节点的前后节点都为null
public LinkedList() {
header.next = header.previous = header;
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
双向循环链表图:
然鹅在jdk1.7后的时候,1.6的header Entry循环链表被替换成了first Node和last Node,那么问题又来了,为什么Josh Bloch(此方法的作者)要将双向循环链表从1.7之后改成了双向链表?
事出必有因,有因必有果,你的报应就是我..........
这个问题,特意去问了群里阿里的一个大佬,他沉默了……后来我也翻阅了各大博客论坛,没有找到一个完美的解释,于是我沉默了…….
但是!!!一位不愿透露姓名的知名网红曾说过:消除恐惧的最好办法就是面对恐惧,奥利给!!!!
看到这里的小伙伴可以先思考一下,为什么Josh Bloch(此方法的作者)要将双向循环链表从1.7之后改成了双向链表?(我的个人观点会在文章结尾总结阐述)
LinkedList jdk1.8 源码解析
不知道大家有没有看源码和看源码注释的习惯,但建议养成这个良好习惯,因为作者会写的很详细,就LinkedList的注释我简单的说一点:
1. LinkedList底层是双链表,并且是List和Deque接口的实现,实现List接口可以有队列操作,实现Deque可以有双端队列的操作
2.注意:LinkedList不是同步的,意味着线程不安全,如果有多个线程同时访问双链表,至少有一个线程在结构上修改list,那就必须在外部加上同步操作synchronized。
3.如果想要LinkedList实现线程安全,应该使用Collections.synchronizedList来封装链表,并且最好在创建时完成,以防止意外的对链表进行非同步的访问。
例子:List list = Collections.synchronizedList(new LinkedList(.....))
就挑这些简单翻译一下,需要知道的重中之重就是 LinkedList非线程安全,ArrayList也是非线程安全。
源码解析:
transient int size = 0; //当前存储的元素个数
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first; //前一个节点元素
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last; //后一个节点元素
/**
* Constructs an empty list.
*/
public LinkedList() {
}
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
同样LinkedList有两个构造函数,有参构造是通过一个集合参数,并且把该集合的所有元素插入到LinkedList中,注意,这里的是“插入”而不是说“初始化添加”,因为它是非线程安全,可能this()调用之后,已经有其它线程向里面插入数据了。
一、 addAll
addAll的实现方法也比较简单,先check索引大小是否越界,将集合转成数组,定义pred(前置节点),succ(后置节点)两个Node对象,用作标识。
if/else判断,判断的目的,可能这个方法是被直接调用的,像这样:
而不是直接从这里调用的,那就有一个问题,需要判断是否从尾部插入的,很好理解,人家都带index了,再从尾部插入,那不扯吗? 所以当index = size表示从尾部插入,后置节点为null,前置节点为当前LinkedList的最后一个节点,否则就根据索引插入相应位置。
简单写了一个小例子,看一下for后的结果值,其实一目了然~~
二、add
这个方法很简单,重点要说的是,一直都在说,新增删除时,ArrayList和LinkedList效率要高,那到底高在哪?理论也说了几遍了,结合代码再来看一下:
不知道小伙伴还记不记得ArrayList添加是怎么实现的了,再温故知新一遍。ArrayList在进行add的时候,先要判断是否需要扩容,如果当前数组为空,则就使用默认容量,否则就进行以1.5倍的方式进行动态扩容,然后使用Array.copyof()进行复制。
那LinkedList呢?
首先LinkedList没有动态扩容那么一说,也没有拷贝的动作,而是链表是没有长度限制的。
一个判断,是否为首次插入,是,first/last都为null,否,就把新节点变成旧节点的后续节点,简单粗暴,简而言之,拒绝一切花里胡哨~~~
三、get
呐~又来到了ArrayList的优点区,LinkedList默默的低下了刚刚高高在上的大额头,同样结合代码来说一下,这里就不对比ArrayList代码了,确实ArrayList的get方法实现很简单。
翠花,上代码~~~
简单理解,可以看到这里折半查询节点,如果index < (size >> 1)则从头部查找,否则从尾部开始查询。
这里有个小问题,为什么当index < (size >> 1)就从头部插入而不是其它值?(我还是真喜欢扣这些常量值的定义,十万个为什么)
首先这里采用了简单的二分算法,判断index和list的中间距离,如果index距离list中间位置较近,则从头部向后遍历,否则,从头部向前遍历。
两者比较,LinkedList查找慢的原因一目了然,这要是成百上千甚至更多, 那编程的小哥哥一顿毒打是少不了了。
四、remove
LinkedList提供了两者remove方法,我说的两种remove是不包括removeFirst()那些.
1.remove(int index) //根据索引删除
返回指定索引处的节点,这个代码是不是很熟悉,没错,就是get方法的公共代码。
将指定节点从链表中移除,来看一下代码实现,第一个if:如果是首节点,将待移除的节点的next属性节点设置为first,如果不是首节点,待移除的节点有上一节点,就将上一节点的next设置为待移除的next。简单来说,就是把待移除节点的next替换待移除节点的位置。
这句解释很绕,像极了百度百科,简单的话非要复杂的说。那就这样来看,
第2个if,道理是一样的,只不过从尾部删除。
2.remove(Object o) //根据value删除
多说一句,LinkedList是允许插入重复值的,但是在remove的时候,是按照重复值的第一个删除,并不是一下会把所有重复值都删除。
简单说一下这里就行,unlink(x)和上述是相同的方法。逻辑也比较清晰,判断o是否为空,找到第一个数据值为null的节点,然后删除。非空,循环,找第一个与o的数据值相等的节点,然后删除。
总结:
本章主要就针对一些比较常用的方法并且主要还是想通过源码解析来看它的增删优点和查询缺点主要体现在哪里,当然还有其它的一些方法,都是大同小异。对于技术我可能还是有些喜欢刨根问底的轴劲,还是知其然先知其所以然。
然后,就我个人而言,ArrayList真的在任何时候新增/删除都比LinkedList效率慢吗?我觉得未必,一定会有一个临界点,如果集合容量在默认值范围内,如果被删除的元素距离尾部很近,我觉得ArrayList会比LinkedList更高一点。至于临界点,还是有待商榷的同时也可以被讨论的一个问题,当然,以上纯属个人论点,如有不同见解,非常欢迎来探讨学习。
最后,我以个人观点来解释一下上述说的问题,为什么Josh Bloch(此方法的作者)在jdk 1.7之后要将双向循环链表从1.7之后改成了双向链表?
从代码的角度区分了几点:
-
first/last对于链头、链尾概念更清晰,代码更容易明白
-
很明显,jdk1.7之后节省了new一个headerEntry的时间
-
在链头链尾进行插入/删除,first/last方式更加快捷
插入/删除都会有两种情况:从中间、从两边
从中间来说,都是一样,先遍历找到index,然后修改链表index处两头的指针。
在两头来说,对于循环链表来说,由于首尾相连,还是需要处理两头的指针。而非循环链表只需要处理一边,前一节点或后一节点。就此情况来说,非循环链表更高效一点。
那为什么要在这篇文章讲了很多LinkedList从jdk 1.6至今的进化史呢?
“当然是闲的慌”
“啊呸,给老子死”
确实,现在jdk1.8已经走进了千家万户,前面也说过jdk1.8算是一个里程碑,特别是针对一些老方法性能上的优化,可能很多程序员根本也就没用过jdk1.6,就包括我也没用过。主要就是看一看它的发展史,多了解一点总没坏处的。
知长远切莫忘根源(哎哟我去,这文邹邹的,我现编的,(河南话:咦,可真带劲,哈哈哈哈)
各位好,我是李小北,我在北京,这个城市最不值钱的东西叫做梦想。
END
来源:网络(侵删)
图片来源:网络(侵删)