JDK原始碼裡的HashMap/LinkedHashMap和自己手寫的HashMap到底有什麼區別?
HashMap特點
HashMap衝突時先拉出一個連結串列,當連結串列節點超過TREEIFY_THRESHOLD,自動進行TREEIFY將連結串列轉換成紅黑樹,將Node轉換成TreeNode
奇妙的內部類繼承關係
- HashMap.TreeNode 繼承 LinkedHashMap.Entry
- LinkedHashMap 繼承 HashMap
- LinkedHashMap.Entry 繼承自HashMap.Node
class testJ{
public static void main(String[] args) {
HashMap hm = new HashMap();
}
}
class HashMap{
class TreeNode extends LinkedHashMap.Entry{
}
static class Node{
}
}
class LinkedHashMap extends HashMap{
static class Entry extends HashMap.Node{
}
}
複製程式碼
[Finished in 1.9s]
HashMap的表的大小一定是2^n
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
複製程式碼
HashMap取址的方式
Node<K,V> p = table[(n - 1) & hash(key)]
n - 1 = 2 ^ x - 1
,那麼反映在二進位制位上就是n - 1 的低位全為1,高位全為0。
HashMap中的hash()
/**
* Computes key.hashCode() and spreads (XORs) higher bits of hash
* to lower. Because the table uses power-of-two masking,sets of
* hashes that vary only in bits above the current mask will
* always collide. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward. There is a tradeoff between speed,utility,and
* quality of bit-spreading. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading),and because we use trees to handle large sets of
* collisions in bins,we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage,as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds.
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
複製程式碼
將鍵物件自身的hashcode進行了一個位操作,應用這個變換,可以將高位的影響傳遞到hashcode中。有效的避免衝突,但有些時候物件的hashcode已經是分佈良好的,那麼,這樣的物件不會從這個變換中獲益。該變換比較適用於比較小的table,因為這樣的table高位全為0。
HashMap判斷是否包含一個物件之getNode(hash(key),key)方法
final Node<K,V> getNode(int hash,Object key) {
Node<K,V>[] tab; Node<K,V> first,e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash,key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
複製程式碼
先取址第一個節點,總是判斷第一個節點是不是和物件key相同(1.hashcode 2. 是否為同一個物件的引用或者equals
)
第一個節點不是要找的物件時,分兩種情況。
- 若這個節點已經是一個TreeNode,那麼呼叫TreeNode的getTreeNode(hash,key)方法在樹中進行二分查詢。
- 若這個節點仍然是Node(普通的連結串列節點),那麼以線性時間執行順序的遍歷。
再雜湊(rehash/resize)
再雜湊的場景: 當put操作時發現表的size已經達到table.length * loadfactor
再雜湊的操作:
- 建立一個新的Node<K,V> [] newTab
- 順序遍歷原來的oldTab,將每個節點重新計算hash,因為表的大小是2^n, 所以hash要麼和之前的保持一致,要麼是之前的兩倍
- 如果當前的節點沒有拉鍊,那麼直接插入。如果當前節點還有後續元素,同樣分兩種情況(tree/linkedlist)。
- 如果是連結串列形式的Node,那麼用
e.hash & oldCap
即可判斷當前的節點是否可以從原來的連結串列中分離出來,插入到newTab中。
Node<K,V> loHead = null,loTail = null;
Node<K,V> hiHead = null,hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
複製程式碼
HashMap鍵值對允許空值的鍵,也允許空值的值
因此用map.get(key) == null
並不能總是正確的等價與containsKey(key)
。
同時,hashtable既不允許null鍵,也不允許null值。會在執行時報出異常。
鑑於Hashtable是早於HashMap出現的,我認為這一點限制是完全可以像HashMap那樣進行改進的。
這也許算是Hashtable的一個缺陷吧,好在Hashtable在併發上因為讀寫方法都加鎖,導致併發效能也不理想的原因,也逐漸不被使用了。
Hashtable部分原始碼:
public synchronized V put(K key,V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
/*
這一行就和HashMap不同,導致這一行當key == null時, 因為直接引用key.hashCode()丟擲異常
而HashMap中呼叫putVal(hash(key),key,value,false,true)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
因此不會因為key為空而丟擲異常
*/
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash,index);
return null;
}
複製程式碼
Map<Integer,Integer> map = new HashMap<Integer,Integer>();
map.put(null,null);
System.out.println(map.get(null)); //null
System.out.println(map.containsKey(null)); //true
複製程式碼
這是因為getNode(hash(key),key)方法只會比較,那個hash & tab.length - 1
位置的node是否為空。
而不管node.K == null,還是node.V == null,都不能說明node == null.
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key),key)) == null ? null : e.value;
}
複製程式碼
也就是說e.value == null時,map中還是有這個鍵值對的。
預留回撥函式的機制,為了繼承自HashMap的LinkedHashMap
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
複製程式碼
預留這些函式的好處: LinkedHashMap在繼承時完全不用重寫基本的put,remove等函式,只用重寫它要用到的這些回撥函式
- afterNodeAccess(),在replace,compute,merge, put函式中呼叫
- afterNodeInsertion(),在put,merge,compute函式中會呼叫
- afterNodeRemoval(),在remove函式中呼叫
get方法中沒有呼叫afterNodeAccess()是因為,在LinkedHashMap中重寫了get方法。因為要根據accessOrder來判斷是否呼叫。
LinkedHashMap
繼承自HashMap,繼承了絕大部分方法
但增加了一個繼承自HashMap.Node的Entry類。維護了一個 雙向連結串列。可以實現元素的順序訪問(兩種順序: accessOrder)。順序訪問依賴於map.entrySet().iterator(),該方法會建立一個該Map所維護的那個雙向連結串列的迭代器,從而以LinkedList的順序訪問Entry。
/**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order,<tt>false</tt> for insertion-order.
* 預設為false
* true: 按訪問順序
* false: 按插入順序
* @serial
*/
final boolean accessOrder;
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before,after;
Entry(int hash,K key,V value,Node<K,V> next) {
super(hash,next);
}
}
複製程式碼
按訪問順序訪問:
Map<Integer,Integer> map = new LinkedHashMap<Integer,Integer>(10,0.75f,true);
map.put(1,2);
map.put(3,4);
map.put(5,6);
map.get(1);
Iterator<Map.Entry<Integer,Integer>> it = map.entrySet().iterator();
while(it.hasNext()){
System.out.println(it.next());
}
複製程式碼
3=4
5=6
1=2
複製程式碼
重寫/實現了HashMap中的回撥方法
void afterNodeAccess(Node<K,V> e) { // move node to last
//omitted
}
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key),null,false,true);
}
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
void afterNodeRemoval(Node<K,V> e) { // unlink
//omitted
}
複製程式碼
afterNodeInsertion方法執行時需要判斷是否需要把最近最少訪問的元素(也就是head)刪除掉。
public V put(K key,V value) {
return putVal(hash(key),true);
}
複製程式碼
判斷條件中evict在put時傳遞的是true,第三個條件是一個函式返回值。這個函式預設返回false,那麼就是永遠不會驅除eldest element。 當我們想要實現LRU時,重寫該方法,即可。
@Override
protected boolean removeEldestEntry(Map.Entry<String,String> eldest) {
return size() > CACHE;
}
複製程式碼
當前的size比規定的CACHE大時,返回true,那麼LinkedHashMap就可以自動的去執行驅除的邏輯了。
重寫了get方法
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key),key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
複製程式碼
當決定訪問順序為true, 即訪問順序時,afterNodeAccess(e)會得到執行,將e這個節點加到雙向連結串列的尾巴上。
利用LinkedHashMap快速實現LRU
public static void main(String[] args) {
int size = 5;
/**
* false,基於插入排序
* true,基於訪問排序
*/
Map<String,String> map = new LinkedHashMap<String,String>(size,.75F,true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String,String> eldest) {
boolean tooBig = size() > size;
if (tooBig) {
System.out.println("recently least key=" + eldest.getKey());
}
return tooBig;
}
};
map.put("1","1");
map.put("2","2");
map.put("3","3");
map.put("4","4");
map.put("5","5");
System.out.println(map.toString());
map.put("6","6");
map.get("2");
map.put("7","7");
map.get("4");
System.out.println(map.toString());
}
複製程式碼
輸出結果:
{1=1,2=2,3=3,4=4,5=5}
recently least key=1
recently least key=3
{5=5,6=6,7=7,4=4}
複製程式碼