前面分析了HashMap的实现,我们知道其底层数据存储是一个hash表(数组+单向链表)。接下来我们看一下另一个LinkedHashMap,它是HashMap的一个子类,他在HashMap的基础上维持了一个双向链表(hash表+双向链表),在遍历的时候可以使用插入顺序(先进先出,类似于FIFO),或者是最近最少使用(LRU)的顺序。
来具体看下LinkedHashMap的实现。
1.定义
1 2 3 |
|
从定义可以看到LinkedHashMap继承于HashMap,且实现了Map接口。这也就意味着HashMap的一些优秀因素可以被继承下来,比如hash寻址,使用链表解决hash冲突等实现的快速查找,对于HashMap中一些效率较低的内容,比如容器扩容过程,遍历方式,LinkedHashMap是否做了一些优化呢。继续看代码吧。
2.底层存储
开篇我们说了LinkedHashMap是基于HashMap,并在其基础上维持了一个双向链表,也就是说LinkedHashMap是一个hash表(数组+单向链表) +双向链表的实现,到底实现方式是怎么样的,来看一下:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
看到了一个无比熟悉的属性header,它在LinkedList中出现过,英文注释很明确,是双向链表的头结点对不对。再看accessOrder这个属性,true表示最近较少使用顺序,false表示插入顺序。当然你说怎么没看到数组呢,别忘了LinkedHashMap继承于HashMap,Entry[]这个东东就不用写了吧。。。
再来看下Entry这个节点类和HashMap中的有什么不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
|
可以看到Entry继承了HashMap中的Entry,但是LinkedHashMap中的Entry多了两个属性指向上一个节点的before和指向下一个节点的after,也正是这两个属性组成了一个双向链表。等等。。。Entry还有一个继承下来的next属性,这个next是单向链表中用来指向下一个节点的,怎么回事嘛,怎么又是单向链表又是双向链表呢,都要晕了对不对,其实想的没错,这里的节点即是Hash表中的单向链表中的一个节点,它又是LinkedHashMap维护的双向链表中的一个节点,是不是瞬间觉得高大上了。图解一下吧(不要告诉我图好乱,我看不懂。。。)
注:黑色箭头指向表示单向链表的next指向,红色箭头指向表示双向链表的before指向,蓝色箭头指向表示双向链表的after指向。另外LinkedHashMap种还有一个header节点是不保存数据的,这里没有画出来。
从上图可以看出LinkedHashMap仍然是一个Hash表,底层由一个数组组成,而数组的每一项都是个单向链表,由next指向下一个节点。但是LinkedHashMap所不同的是,在节点中多了两个属性before和after,由这两个属性组成了一个双向循环链表(你怎么知道是循环,下面在说喽),而由这个双向链表维持着Map容器中元素的顺序。看下Entry中的recordRemoval方法,该方法将在节点被删除时候调用,Hash表中链表节点被正常删除后,调用该方法修正由于节点被删除后双向链表的前后指向关系,从这一点来看,LinkedHashMap比HashMap的add、remove、set等操作要慢一些(因为要维护双向链表 )。
明白了LinkedHashMap的底层存储结构后,我们来看一下它的构造方法以及怎么样对链表进行初始化的。
3.构造方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
构造方法很简单基本都是调用父类HashMap的构造方法(super),只有一个区别就是对于accessOrder的设定,上面的构造参数中多数都是将accessOrder默认设置为false,只有一个构造方法留了一个出口可以设置accessOrder参数。看完了构造方法,发现一个问题,咦?头部节点header的初始化跑哪里去了?
回忆一下,看看HashMap的构造方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
哦,明白了,init()在HashMap中是一个空方法,也就是给子类留的一个回调函数,ok,我们来看下LinkedHashMap对init()方法的实现吧。
1 2 3 4 5 6 7 8 9 10 |
|
init()方法看完了,看出点什么嘛?LinkedHashMap中维护的是个双向循环链表对不对?(什么?还不明白,去好好看看给jdk写注释系列之jdk1.6容器(2)-LinkedList源码解析)
4.增加
LinkedHashMap没有重写put方法,只是重写了HashMap中被put方法调用的addEntry。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
可以看到,在添加方法上,比HashMap中多了两个逻辑,一个是当Map容量不足后判断是删除第一个元素,还是进行扩容,另一个是维护双向链表。而在判断是否删除元素的时候,我们发现removeEldestEntry这个方法竟然是永远返回false,这什么鬼。。。哦,想了下,原来想要实现Cache功能,需要自己继承LinkedHashMap然后重写removeEldestEntry方法,这里默认提供的是容器的功能。
5.删除
LinkedHashMap没有重写remove方法,只是在实现了Entry类的recordRemoval方法,该方法是HashMap的提供的一个回调方法,在HashMap的remove方法进行回调,而LinkedHashMap中recordRemoval的主要当然是要维护双向链表了,返回上面去看下Entry类的recordRemoval方法吧。
6.查找
LinkedHashMap重写了get方法,但是确复用了HashMap中的getEntry方法,LinkedHashMap是在get方法中指加入了调用recoreAccess方法的逻辑,recoreAccess方法的目的当然也是维护双向链表了,具体逻辑返回上面去看下Entry类的recoreAccess方法吧。
1 2 3 4 5 6 7 |
|
7.是否包含
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
LinkedHashMap对containsValue进行了重写,我们在HashMap中说过,HashMap的containsValue需要遍历整个hash表,这样是十分低效的。而LinkedHashMap中重写后,不再遍历hash表,而是遍历其维护的双向链表,这样在效率上难道就有所改善吗?我们分析下:hash表是由数组+单向链表组成,而由于使用hash算法,可能会导致散列不均匀,甚至数组的有些项是没有元素的(没有hash出对应的散列值),而LinkedHashMap的双向链表呢,是不存在空项的,所以LinkedHashMap的containsValue比HashMap的containsValue效率要好一些。