LInkedHashMap是基于HashMap的,因此如果不太清楚HashMap的实现的话,请先阅读HashMap 源码阅读
我们都知道,HashMap 是无序的,也就是说,遍历时候的顺序与访问的顺序无关.
而在一些场景下,我们即需要HashMap的特性,又需要它能够保持一定的顺序呢?
JAVA 在 JDK1.4 以后提供了 LinkedHashMap 来帮助我们实现了有序的 HashMap!
LinkedHashMap可以有两种保存的顺序:插入顺序及访问顺序.
在如下的代码中,我们以1,2,4,3
的顺序分别向HashMap
,插入顺序的LinkedHashMap
,访问顺序的LinkedHashMap
中插入四条数据,并在访问顺序的LinkedHashMap
中按照4231
的顺序访问元素.
代码如下:
1 | public void linkedHashMapTest() { |
输出结果如下:
1 | LinkedHashMap:1243 |
可以看到,HashMap是无序的,遍历结果的顺序和插入顺序无关,是key
值的自然排序,而LinkedHashMap的顺序是插入顺序或者访问顺序.
LInkedHashMap怎么保存顺序的?
在HashMap的基础上,对每个节点添加指向上一个元素和下一个元素的”指针”,这样在数据的保存时,仍使用HashMap的原理.同时,添加向前向后指针,使得所有的节点形成双向链表,在遍历时使用双向链表遍历,保证顺序.
源码阅读
下面将通过阅读LinkedHashMap的源码来学习使用这个类.
类的定义
1 | public class LinkedHashMap<K,V> |
LinkedHashMap集成了HashMap类以及实现了Map接口,那么LinkedHashMap作为HashMap的子类,天然集成了HashMap的所有方法,也拥有了HashMap的一切特性.
类的成员
1 | /** |
LInkedHashMap新增了三个成员变量,首先是双链表的头部节点和尾部节点.然后是一个标识位,标识此LinkedHashMap使用插入顺序还是访问顺序.
那么LinkedHashMap.Entry
是什么呢?
1 | static class Entry<K,V> extends HashMap.Node<K,V> { |
他继承自HashMap的内部类Node,之后又添加了指向上一节点的before和指向下一节点的after,这样就形成了双向链表,用来记录元素的顺序.
构造方法
LinkedHashMap的构造方法共有5个,除了最后一个,其他的在内部实现都是调用了父类HashMap
对应的构造方法,并将标识位accessOrder
置为false.(默认值即false).在最后一个构造方法中,将accessOrder
置为参数传入的值.
get()方法
1 | public V get(Object key) { |
LinkedHashMap对get方法进行了重写,具体流程为:
- 调用父类HashMap的的getNode()方法,如果结果值为空,则返回空.
- 如果
accessOrder
为true,则调用afterNodeAccess()
方法将当前访问的元素移动到链表的末尾.(当初始化为访问顺序的LinkedHashMap时,才会执行此操作.) - 如果
accessOrder
为false,返回getNode()
获得的值.
put()方法
1 | Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) { |
LInkedHashMap并没有重写put()
方法,但是重写了put()方法中会调用的newNode()方法,代码如上面所示.
在重写后的newNode()
方法中,调用父类的构造方法新建一个节点后,调用linkNodeLast()
方法,将新插入的节点链接在双链表的尾部.
remove()方法
1 | void afterNodeRemoval(Node<K,V> e) { // unlink |
LinkedHashMap也没有重写remove()
方法,但是对remove方法中removeNode()
方法中调用的回调函数afterNodeRemoval进行了重写.
在按照HashMap的方式删除节点后,将该节点同步的从双链表中移除掉.
containsVaule()方法
1 | public boolean containsValue(Object value) { |
想必与HashMap
,LinkedHashMap重写后的containsVaule()
更为高效一些,直接在双链表中进行遍历判断是否存在value相等的值.
forEach
1 | public void forEach(BiConsumer<? super K, ? super V> action) { |
LinkedHashMap的遍历方式上面的代码所示:
直接从双链表的头部开始遍历,逐个输出即可.
总结
在读懂了HashMap的源码后,LinkedHashMap会显得比较简单,因为他的大多数操作都是在HashMap的基础上完成的.
LinkedHashMap有几个比较关键的问题如下:
1.如何实现的元素有序?
在HashMap的基础上,对每一个节点添加向前向后指针,这样所有的节点形成了双向链表,自然就是有序的.
2.如何保证顺序的正确以及同步
通过重写的一些关键的方法,在元素发生增删改查
等行为时,除了在Hash桶上进行操作,也对链表进行相应的更新,以此来保证顺序的正确.
3.如何实现两种顺序(插入顺序或者访问顺序)?
通过内部的标识位accessOrder
来记录当前LinkedHashMap是以什么为序,之后再处理元素时通过读取accessOrder
的值来控制链表的顺序.
4.为什么重写containsValue()
而不重写containsKey()
?
可以看一下他们分别是怎么实现的,就知道原因了.
在HashMap中:
1 | public boolean containsValue(Object value) { |
containsKey()是通过hash值直接计算出该key对应的数组下标,之后在该hash桶的链表上进行查找相同的key.
containsValue()是对table进行遍历,对其中的每一个hash桶的所有值进行遍历,去寻找相同的value.
而在LinkedHashMap中,如果都改为对双向链表的遍历来寻找key和value.
无疑在value的查找时会有性能的提升,而对于key的查找则更为低效了.
如果改为遍历双向链表进行查找key值,则从key->hash->index
的方法退化到逐一遍历,丧失了HashMap的最大特性.
完。
ChangeLog
2018-12-16 完成以上皆为个人所思所得,如有错误欢迎评论区指正。
欢迎转载,烦请署名并保留原文链接。
更多学习笔记见个人博客——>呼延十