LinkedHashMap源码详解
来源:互联网 发布:安卓微信数据迁移 编辑:程序博客网 时间:2024/06/03 18:54
LinkedHashMap源码详解
LinkedHashMap是基于HashMap实现的,如果对HashMap不了解,请先学习HashMap:http://blog.csdn.net/luanmousheng/article/details/75195809
HashMap的无序性和LinkedHashMap的有序性
前面介绍的HashMap查找效率很高,但是也有一个缺点,即遍历HashMap是无序的。LinkedHashMap顾名思义,是链表和哈希表的结合,链表具有天然的有序性,这里的有序不是按照节点大小排序,而是按照节点的插入顺序排序或者节点的访问顺序排序。
为了比较HashMap和LinkedHashMap的有序性,首先观察HashMap的遍历结果:
Map<String, String> map2 = new HashMap(); map2.put("name", "jack"); map2.put("age", "23"); map2.put("job", "student"); map2.put("home", "china"); Iterator<Map.Entry<String, String>> it2 = map2.entrySet().iterator(); while(it2.hasNext()) { System.out.println(it2.next().getKey()); }
这段代码段输出:
homeagenamejob
可以看到,遍历HashMap时的输出和输入时的顺序没有关系。
再观察LinkedHashMap的遍历结果:
LinkedHashMap<String, String> map = new LinkedHashMap(); map.put("name", "jack"); map.put("home", "china"); map.put("age", "23"); map.put("job", "student"); Iterator<Map.Entry<String, String>> it = map.entrySet().iterator(); while(it.hasNext()) { System.out.println(it.next().getKey()); }
这段代码输出:
namehomeagejob
可以看到,默认情况下,遍历LinkedHashMap时的输出和输入的顺序是一致的。因此我们说LinkedHashMap是可以保证有序性的。
我们说默认情况下,遍历LinkedHashMap时的输出和输入的顺序是一致的,LinkedHashMap还可以根据节点的访问顺序进行排序,即最新访问的节点放在最前面。LinkedHashMap提供了accessOrder字段,这个字段可以指示LinkedHashMap是否按照访问时间进行排序,通过LinkedHashMap另一个带参数的构造函数可以创建一个按照访问时间排序的哈希表,看下面的例子:
LinkedHashMap<String, String> map = new LinkedHashMap(10, 0.75F, true); map.put("name", "jack"); map.put("home", "china"); map.put("age", "23"); map.put("job", "student"); //访问了键"home"的节点 map.get("home"); Iterator<Map.Entry<String, String>> it = map.entrySet().iterator(); while(it.hasNext()) { System.out.println(it.next().getKey()); }
将4个键值对添加到LinkedHashMap后,访问了键”home”的节点,这段代码的输出为:
nameagejobhome
可以看到,键”home”被我们访问后,放到了最后一个位置(最新的位置),这就是LinkedHashMap按照访问先后顺序的有序性。
好了,对于LinkedHashMap有了基本的认识后,下面将基于源码,详细介绍LinkedHashMap的原理。
LinkedHashMap原理
LinkedHashMap的存储还是通过HashMap实现的,但是它和HashMap最大的区别在于LinkedHashMap维护了一个双向链表,这个双向链表按照节点的插入顺序保存节点、或者按照节点的访问先后顺序保存节点。
先看下LinkedHashMap的声明:
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> { //…… private static class Entry<K,V> extends HashMap.Entry<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, HashMap.Entry<K,V> next) { super(hash, key, value, next); } //…… }}
LinkedHashMap继承了HashMap,内部静态类Entry继承了HashMap的Entry,但是它和HashMap.Entry不同的是,LinkedHashMap.Entry多了两个字段:before和after,before表示在本节点之前添加到LinkedHashMap的那个节点,after表示在本节点之后添加到LinkedHashMap的那个节点,这里的之前和之后指时间上的先后顺序。
有了对LinkedHashMap.Entry的了解,通过示意图学习LinkedHashMap的工作原理。
图1 LinkedHashMap初始状态
LinkedHashMap的初始状态包括一个HashMap和一个只有头节点的双向链表。
接着插入键K1:
图2 插入键K1的状态
接着插入键K2:
图3 插入键K2的状态
通过以上三个示意图,基本上可以理解LinkedHashMap的工作原理,示意图的左边部分是HashMap,右边部分是双向链表,这个双向链表记录了键的添加顺序。注意,这里我们将HashMap中的键和链表中的键分开表示,其实它们是同一个节点,分开后利于观察,否则很多指针纠缠在一起,示意图会显得很混乱。
以上三个示意图都是基于插入顺序排序的,假设当前LinkedHashMap的状态如图3,并且我们创建该哈希表时候指定了按访问时间排序,当我们在图3的基础上分别添加K3、访问K2后的状态为:
图4 分别添加K3、访问K2后的状态
在图3的基础上,添加K3、访问K2后将K2移到了链表的末尾。
将LinkedHashMap的accessOrder字段设置为true后,每次访问哈希表中的节点都将该节点移到链表的末尾,表示该节点是最新访问的节点。
好了,我们通过几个示意图已经了解了LinkedHashMap的工作原理,接着学习LinkedHashMap的源码。
LinkedHashMap源码解析
我们使用哈希表是为了往哈希表中添加键值对,那我们就从最基本的方法put说起。
put方法
图5 LinkedHashMap中搜索put方法
在idea中搜索LinkedHashMap的put方法,惊奇的发现,LinkedHashMap中并没有定义put方法,相反,idea向我们推荐了很多put的实现,显然我们应该去看HashMap的put实现,也就是说,LinkedHashMap的方法即是HashMap的put方法,当我们调用LinkedHashMap的put方法实际上调用的是父类HashMap的put方法。
看下HashMap的put实现:
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; //1:HashMap中是个空实现,子类LinkedHashMap的Entry实现了该方法 e.recordAccess(this); return oldValue; } } modCount++; //2:子类LinkedHashMap重写了该方法 addEntry(hash, key, value, i); return null;}
上面代码1处和2处正是LinkedHashMap的不同之处,LinkedHashMap重写了HashMap.Entry的recordAccess方法和HashMap的addEntry方法。其中recordAccess方法在父类HashMap的Entry是个空实现,子类LinkedHashMap.Entry重写该方法是为了记录节点访问的先后顺序。
是时候介绍LinkedHashMap的Entry类了:
private static class Entry<K,V> extends HashMap.Entry<K,V> { //before节点在当前节点之前插入 //after节点在当前节点之后插入 Entry<K,V> before, after; Entry(int hash, K key, V value, HashMap.Entry<K,V> next) { //直接调用了父类的构造函数 super(hash, key, value, next); } //将当前节点从该双向链表中删除 private void remove() { before.after = after; after.before = before; } //将当前节点插入指定节点的前面 //其实就是改变链表的指针指向 private void addBefore(Entry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this; after.before = this; } //这是LinkedHashMap与HashMap重要的不同之处 void recordAccess(HashMap<K,V> m) { LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; if (lm.accessOrder) { //若是节点访问先后顺序的规则 lm.modCount++; //先把当前节点删除,然后把该节点添加到头节点的前面,也就是链表的末尾,参考示意图2和图3 remove(); addBefore(lm.header); } } void recordRemoval(HashMap<K,V> m) { remove(); }}
这段代码最重要之处在于对accessOrder的判断,若是基于访问顺序,每次访问节点后需要将该节点移到链表的末尾处,否则recordAccess什么也不做。其他的代码就是对双向链表指针指向的改变,参考图2、3、4。
accessOrder的意义在于,每次访问一个节点都将该节点移到链表的末尾,表示这个节点是最新访问的节点,这在很多场景下都很有用处,比如LRU算法,排在链表末尾部分的节点都是最近使用过的节点,那么排在前面部分的节点就可能长期都没有被访问过,此时系统可以将这些节点删除以增加可用内存。
好了,现在继续看put方法调用的addEntry方法。子类LinkedHashMap重写了HashMap的addEntry方法,看下LinkedHashMap的addEntry方法:
void addEntry(int hash, K key, V value, int bucketIndex) { //调用了父类HashMap的addEntry方法 super.addEntry(hash, key, value, bucketIndex); // 如果需要的话删除在链表中最久的节点 Entry<K,V> eldest = header.after; //removeEldestEntry是个protected方法。 //LinkedHashMap中该方法返回false,也就是不会删除在链表中最久的节点。 if (removeEldestEntry(eldest)) { removeEntryForKey(eldest.key); }}
LinkedHashMap的addEntry方法首先调用了父类的addEntry方法,注意,子类可以重写removeEldestEntry方法并返回true,删除在链表中最久的节点。
到此为止,似乎并没有找到令我们多么兴奋的事,毕竟LinkedHashMap的removeEldestEntry返回了false,也就是LinkedHashMap只是调用了父类HashMap的addEntry方法,并没有做其他的事。继续深究HashMap的addEntry方法,能发现一些不同的地方:
void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); hash = (null != key) ? hash(key) : 0; bucketIndex = indexFor(hash, table.length); } //LinkedHashMap重写了该方法 createEntry(hash, key, value, bucketIndex);}
LinkedHashMap重写了父类的createEntry方法,让我们继续看LinkedHashMap的createEntry方法:
void createEntry(int hash, K key, V value, int bucketIndex) { HashMap.Entry<K,V> old = table[bucketIndex]; Entry<K,V> e = new Entry<>(hash, key, value, old); table[bucketIndex] = e; //1:将节点e加入到header的前面,也就是链表的末尾 e.addBefore(header); size++;}
比较HashMap和LinkedHashMap,我们发现LinkedHashMap在1处有不同,其他地方都完全相同。
LinkedHashMap创建结点并将该节点添加到HashMap后,该节点会被链接到头结点header链表的末尾,在这里实现了LinkedHashMap插入的有序性。
构造函数
到这里解释完了put方法,接下来看下LinkedHashMap的构造方法,LinkedHashMap有很多重载的构造方法,原理大致大同,这里介绍其中一个构造方法来讲解:
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder;}
这个带3个参数的构造函数其中initialCapacity和loadFactor决定了哈希表的初始容量和加载因子,accessOrder决定了LinkedHashMap的排序规则,如果accessOrder=false,则按插入顺序排序,否则按访问顺序排序。
该构造函数调用了父类的构造函数,看下HashMap中的构造函数:
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; threshold = initialCapacity; //HashMap中该方法是个空方法,子类LinkedHashMap重写了该方法 init();}
父类HashMap中的init方法是个空实现,子类LinkedHashMap重写了该方法,看下LinkedHashMap中init方法的实现:
void init() { header = new Entry<>(-1, null, null, null); header.before = header.after = header;}
该方法创建了一个只带头节点的链表,参考图1。
LinkedHashMap的所有构造函数都会调用父类HashMap的构造函数,HashMap的构造函数都会调用init方法,即我们创建LinkedHashMap时总会调用init方法创建一个只带头节点的链表。
get方法
public V get(Object key) { Entry<K,V> e = (Entry<K,V>)getEntry(key); if (e == null) return null; //看这里,LinkedHashMap的不同之处 e.recordAccess(this); return e.value;}
这里需要注意e.recordAccess的调用,这个调用之前我们已经分析过,每次访问一个节点的时候都要根据accessOrder是否为true,决定是否将该节点移到链表末尾,表示该节点是最近访问的节点,实现按照访问顺序的有序性。
containsValue方法
public boolean containsValue(Object value) { if (value==null) { //在链表中查找值为null的节点 for (Entry e = header.after; e != header; e = e.after) if (e.value==null) return true; } else { //在链表中查找值为value的节点 for (Entry e = header.after; e != header; e = e.after) if (value.equals(e.value)) return true; } return false;}
LinkedHashMap的containsValue方法利用了该哈希表的链表特性,在链表中查找是否存在对应的值。
transfer方法
void transfer(HashMap.Entry[] newTable, boolean rehash) { int newCapacity = newTable.length; //利用LinkedHashMap的链表特性将节点放到新的哈希表 for (Entry<K,V> e = header.after; e != header; e = e.after) { if (rehash) e.hash = (e.key == null) ? 0 : hash(e.key); int index = indexFor(e.hash, newCapacity); e.next = newTable[index]; newTable[index] = e; }}
该方法将原来哈希表中的Entry转移到新的哈希表,通过遍历链表将节点放到新的哈希表中。
迭代器
LinkedHashMap的迭代器是通过它的内部类实现的,其中最主要的类是LinkedHashIterator,该类是个抽象类,提供了基本的迭代方法。Entry迭代器、Key迭代器和Value迭代器都是通过继承该抽象类实现的。首先看下LinkedHashIterator:
private abstract class LinkedHashIterator<T> implements Iterator<T> { //从链表头结点的后一个节点开始遍历 Entry<K,V> nextEntry = header.after; //保存了最近访问到的节点 Entry<K,V> lastReturned = null; //该字段用于快速失败机制,当迭代器发现这两个值不相等,说明有其他线程改变了 //该哈希表,抛出ConcurrentModificationException int expectedModCount = modCount; //如果下个节点是头节点,说明遍历结束 public boolean hasNext() { return nextEntry != header; } public void remove() { //最近返回的节点为null,不能删除 if (lastReturned == null) throw new IllegalStateException(); //快速失败机制,抛出并发修改异常 if (modCount != expectedModCount) throw new ConcurrentModificationException(); LinkedHashMap.this.remove(lastReturned.key); //每迭代一次只能删除一次,不能迭代一次删除多次 lastReturned = null; expectedModCount = modCount; } Entry<K,V> nextEntry() { //快速失败机制 if (modCount != expectedModCount) throw new ConcurrentModificationException(); //不存在下个节点 if (nextEntry == header) throw new NoSuchElementException(); //保存最近访问的节点 Entry<K,V> e = lastReturned = nextEntry; nextEntry = e.after; return e; }}
LinkedHashIterator还是比较好理解的,和HashMap的迭代器类似,直接看源码注释好了。
EntryIterator、KeyIterator和ValueIterator都是通过继承LinkedHashIterator实现的:
private class KeyIterator extends LinkedHashIterator<K> { //键的迭代器 public K next() { return nextEntry().getKey(); }}private class ValueIterator extends LinkedHashIterator<V> { //值的迭代器 public V next() { return nextEntry().value; }}private class EntryIterator extends LinkedHashIterator<Map.Entry<K,V>> { //Entry的迭代器 public Map.Entry<K,V> next() { return nextEntry(); }}
总结
好了,LinkedHashMap的源码就解释完了,做下总结:
- LinkedHashMap是双向链表和HashMap的完美结合。
- LinkedHashMap是有序的,默认通过插入顺序排序,也可以通过构造函数的参数accessOrder指定通过访问顺序排序。
- 只要理解了HashMap的工作原理,就很容易理解LinkedHashMap,它只是在HashMap的基础上将各个Entry节点通过双链表链接起来实现有序性。
- Android LinkedHashMap源码详解
- LinkedHashMap源码详解
- LinkedHashMap源码详解
- 基础知识(二) LinkedHashMap 源码详解
- 史上最详细的LinkedHashMap详解--源码分析
- Java集合——LinkedHashMap源码详解
- LinkedHashMap源码
- LinkedHashMap详解
- 【源码】LinkedHashMap源码剖析
- 【源码】LinkedHashMap源码剖析
- HashMap LinkedHashMap源码分析
- LinkedHashMap源码分析
- LinkedHashMap源码浅析
- 【源码学习-LinkedHashMap】
- LinkedHashMap源码分析
- LinkedHashMap源码浅析
- LinkedHashMap源码阅读
- LinkedHashMap源码分析
- SQL解惑-谜题32计算税收
- [python爬虫] Selenium高级篇之窗口移动、弹出对话框自登录
- Machine learning 5---贝叶斯分类
- 洛谷P1064 金明的预算方案
- 8月10日训练笔记
- LinkedHashMap源码详解
- Spring3.1.0实现原理分析(二十).Dao事务分析之非入侵式编码
- mybatis中延迟加载Lazy策略
- 前端笔试面试题整理
- 找出数组前N大的数
- HDU-Oil Deposits (DFS,BFS,水题)
- Hadoop入门(四)之hadoop集群搭建(一主二从)
- 进程通信
- Linux系统中管理用户登录及登录信息的处理