摘要:擴展的節點包括和,加入兩個域組織額外的雙向鏈表保存順序。實現迭代器相關邏輯,因為迭代器是根據雙向鏈表順序迭代的。
HashMap作為一種經典的數據結構,其根據key定位元素能達到平均O(1)的時間復雜度。 但是,存儲于HashMap中的元素顯然是無序的,遍歷HashMap的順序得看臉。。。
那如何使得HashMap里的元素變得有序呢?一種思路是,將存放HashMap元素的節點,使用指針將他們串起來。換言之,就像在HashMap里面“嵌入”了一個鏈表一樣。
實際上,jdk的LinkedHashMap就是使用這種思路實現的。
LinkedHashMap中的代碼不算多,這是因為,jdk的設計使用繼承復用了代碼,在jdk的設計中,LinkedHashMap是HashMap的擴展:
public class LinkedHashMap對節點進行擴展 父類HashMap中的節點extends HashMap implements Map { /* ... */ }
回想一下HashMap的實現方式中,將key和value打包成的節點有兩種:
第一種,傳統分離鏈表法的鏈表節點。
static class Nodeimplements Map.Entry { /* ... */ }
第二種,HashMap為進行優化,一定情況下會將鏈表重構為紅黑樹。第二種節點是紅黑樹節點:
static final class TreeNodeextends LinkedHashMap.Entry { /* ... */ }
突然發現,HashMap的TreeNode是繼承至LinkedHashMap的Entry的。。。
個人觀點是jdk這種做法不是很優雅,本身LinkedHashMap繼承HashMap就使得兩者之間的邏輯混在了一起,而這里的內部實現又反過來繼承,邏輯搞得很混亂。
LinkedListHashMap需要將節點串成一個“嵌入式”雙向鏈表,因此需要給這兩種節點增加兩個字段:
static class Entryextends HashMap.Node { Entry before, after; Entry(int hash, K key, V value, Node next) { super(hash, key, value, next); } }
擴展HashMap.Node,增加雙向鏈表字段。
由于TreeNode是繼承自LinkedListMap.Entry的,所以它也有這兩個字段。
再來看下LinkedHashMap中的屬性,很少:
/** * The head (eldest) of the doubly linked list. */ transient LinkedHashMap.Entryhead; /** * The tail (youngest) of the doubly linked list. */ transient LinkedHashMap.Entry tail;
記錄雙向鏈表的表頭和表尾。從注釋中可以看出,head節點是最老的,tail節點是最新的,也即鏈表按照由老到新的順序串起來。
最后,由于LinkedHashMap是可以設置它組織元素的順序。一種是鏈表中的元素是按插入時候的順序排序,另外一種是按照訪問的順序排序。
// 指定順序是按照訪問順序來,還是插入順序來 final boolean accessOrder;
這個accessOrder指定是否按插入順序來。
重寫創建節點的函數由于對Map中的節點進行了擴展,因此,在創建節點時不能使用原來的節點了,而應該使用重新創建后的。
HashMap將創建節點的操作抽取出來放到了多帶帶的函數中,LinkedHashMap重寫即可:
Node在獲取、插入、刪除元素時維護雙向鏈表newNode(int hash, K key, V value, Node e) { LinkedHashMap.Entry p = new LinkedHashMap.Entry (hash, key, value, e); linkNodeLast(p); return p; } Node replacementNode(Node p, Node next) { LinkedHashMap.Entry q = (LinkedHashMap.Entry )p; LinkedHashMap.Entry t = new LinkedHashMap.Entry (q.hash, q.key, q.value, next); transferLinks(q, t); return t; } // HashMap的TreeNode是繼承自LinkedHashMap.Entry的,因此能夠參與組織雙向鏈表 TreeNode newTreeNode(int hash, K key, V value, Node next) { TreeNode p = new TreeNode (hash, key, value, next); linkNodeLast(p); return p; } TreeNode replacementTreeNode(Node p, Node next) { LinkedHashMap.Entry q = (LinkedHashMap.Entry )p; TreeNode t = new TreeNode (q.hash, q.key, q.value, next); transferLinks(q, t); return t; }
接下來,則需要在LinkedHashMap的操作時維護雙向鏈表。
刪除回顧下HashMap的源代碼,我們知道,HashMap在刪除節點后,會調用afterNodeRemoval函數。
這個函數在HashMap中是空的,實際上jdk是將它設計為一個hook,果然,在LinkedHashMap中,就重寫了該函數,在其中維護雙向鏈表:
// 當有節點被刪除(即有元素被移除),那么也要將它從雙向鏈表中移除 void afterNodeRemoval(Node插入e) { // unlink LinkedHashMap.Entry p = (LinkedHashMap.Entry )e, b = p.before, a = p.after; p.before = p.after = null; if (b == null) head = a; else b.after = a; if (a == null) tail = b; else a.before = b; }
按照類似的思路,HashMap中在插入元素后會調用afterNodeInsertion,那是不是LinkedHashMap也在這里實現了相關邏輯,插入元素后維護雙向鏈表節點呢?
void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entryfirst; if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; removeNode(hash(key), key, null, false, true); } }
然而,實際上在LinkedHashMap中該函數似乎沒有什么用。因為:
protected boolean removeEldestEntry(Map.Entryeldest) { return false; }
removeEldestEntry始終返回false,afterNodeInsertion相當于什么也沒做。這個邏輯設計目的是什么,還不能很清楚。也許也是為了讓誰去繼承?以后再探究。
那插入元素后的在哪里維護了雙向鏈表呢?回到之前的newNode和newTreeNode:
NodenewNode(int hash, K key, V value, Node e) { LinkedHashMap.Entry p = new LinkedHashMap.Entry (hash, key, value, e); linkNodeLast(p); return p; } TreeNode newTreeNode(int hash, K key, V value, Node next) { TreeNode p = new TreeNode (hash, key, value, next); linkNodeLast(p); return p; } // 尾插雙向鏈表節點 private void linkNodeLast(LinkedHashMap.Entry p) { LinkedHashMap.Entry last = tail; tail = p; if (last == null) head = p; else { p.before = last; last.after = p; } }
由于HashMap中調用newNode時候都是為了裝新插入的元素,所以在這里維護雙向鏈表。
感覺耦合是不是太緊了。。。如果HashMap由于某個操作需要臨時搞個newNode借用下,豈不是會出問題?
下面是replacementNode和replacementTreeNode。
replacementNode在HashMap中的作用是,該K V之前是被TreeNode包裝的,現在需要拿Node包裝它。這也勢必會影響雙向鏈表的結構,所以這里也需要額外維護下。
獲取的時候,同樣,是重寫了`afterNodeAccess`鉤子,這樣在HashMap的獲取邏輯結束后,這里的邏輯會被執行,維護雙向鏈表。 void afterNodeAccess(Nodee) { // move node to last LinkedHashMap.Entry last; if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry p = (LinkedHashMap.Entry )e, b = p.before, a = p.after; p.after = null; if (b == null) head = a; else b.after = a; if (a != null) a.before = b; else last = b; if (last == null) head = p; else { p.before = last; last.after = p; } tail = p; ++modCount; } }
LinkedHashMap中的順序有訪問序和插入序,只有訪問序才需要在訪問的時候更新雙向鏈表結構。也即accessOrder為true才會執行這段邏輯。
最后,注意到:
++modCount; }
一般來說,只有修改了Map結構的操作,才需要修改modCount以讓正在迭代的迭代器感知到了變化。
但是這里,由于迭代器是使用這里的“嵌入式”雙向鏈表進行迭代,而在這里會改變雙向鏈表的結構,若迭代器繼續迭代會造成不可預測的結果。
所以,這里需要改變modCount,阻止迭代器繼續迭代。
LinkedHashMap的一個典型應用場景是LRU算法。
由于現在夜已深,現在不敢熬夜身體吃不消,想睡覺了。所以這個坑以后再填
最后LinkedHashMap還有其它的一些實現細節,如:
clear的時候也要同時維護雙向鏈表;
根據雙向鏈表實現迭代器。
最后,總結下jdk中對LinkedHashMap中的實現思路:
擴展HashMap實現。
擴展HashMap的節點(包括Node和TreeNode),加入兩個域組織額外的雙向鏈表保存順序。
在產生插入、刪除、訪問的地方維護雙向鏈表,通過重寫某些方法實現。
實現迭代器相關邏輯,因為迭代器是根據雙向鏈表順序迭代的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/77006.html
摘要:如今行至于此,當觀賞一方。由于所返回的無執行意義。源碼閱讀總體門檻相對而言比,畢竟大多數底層都由實現了。比心可通過這篇文章理解創建一個實例過程圖工作原理往期線路回顧源碼一帶一路系列之源碼一帶一路系列之源碼一帶一路系列之 本文以jdk1.8中LinkedHashMap.afterNodeAccess()方法為切入點,分析其中難理解、有價值的源碼片段(類似源碼查看是ctrl+鼠標左鍵的過程...
摘要:關于的源碼分析,本文并不打算展開講了。大家可以參考我之前的一篇文章源碼詳細分析。在刪除節點時,父類的刪除邏輯并不會修復所維護的雙向鏈表,這不是它的職責。在節分析鏈表建立過程時,我故意忽略了部分源碼分析。 1. 概述 LinkedHashMap 繼承自 HashMap,在 HashMap 基礎上,通過維護一條雙向鏈表,解決了 HashMap 不能隨時保持遍歷順序和插入順序一致的問題。除此...
摘要:三系列用于保存鍵值對,無論是,還是已棄用的或者線程安全的等,都是基于紅黑樹。是完全基于紅黑樹的,并在此基礎上實現了接口。可以看到,只有紅黑樹,且紅黑樹是通過內部類來實現的。 JDK容器 前言 閱讀JDK源碼有段時間了,準備以博客的形式記錄下來,也方便復習時查閱,本文參考JDK1.8源碼。 一、Collection Collection是所有容器的基類,定義了一些基礎方法。List、Se...
摘要:若遇到哈希沖突,則將沖突的值加到鏈表中即可。之后相比于之前的版本,之后在解決哈希沖突時有了較大的變化,當鏈表長度大于閾值默認為時,將鏈表轉化為紅黑樹,以減少搜索時間。有序,唯一紅黑樹自平衡的排序二叉樹。 本文是最最最常見Java面試題總結系列第三周的文章。主要內容: Arraylist 與 LinkedList 異同 ArrayList 與 Vector 區別 HashMap的底層...
閱讀 2104·2023-04-25 20:52
閱讀 2497·2021-09-22 15:22
閱讀 2128·2021-08-09 13:44
閱讀 1772·2019-08-30 13:55
閱讀 2813·2019-08-23 15:42
閱讀 2287·2019-08-23 14:14
閱讀 2880·2019-08-23 13:58
閱讀 3009·2019-08-23 11:49