1. 程式人生 > 程式設計 >JDK原始碼裡的HashMap/LinkedHashMap和自己手寫的HashMap到底有什麼區別?

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永遠是2 ^ x,所以 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) 第一個節點不是要找的物件時,分兩種情況。

  1. 若這個節點已經是一個TreeNode,那麼呼叫TreeNode的getTreeNode(hash,key)方法在樹中進行二分查詢。
  2. 若這個節點仍然是Node(普通的連結串列節點),那麼以線性時間執行順序的遍歷。

再雜湊(rehash/resize)

再雜湊的場景: 當put操作時發現表的size已經達到table.length * loadfactor 再雜湊的操作:

  1. 建立一個新的Node<K,V> [] newTab
  2. 順序遍歷原來的oldTab,將每個節點重新計算hash,因為表的大小是2^n, 所以hash要麼和之前的保持一致,要麼是之前的兩倍
  3. 如果當前的節點沒有拉鍊,那麼直接插入。如果當前節點還有後續元素,同樣分兩種情況(tree/linkedlist)。
  4. 如果是連結串列形式的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等函式,只用重寫它要用到的這些回撥函式

  1. afterNodeAccess(),在replace,compute,merge, put函式中呼叫
  2. afterNodeInsertion(),在put,merge,compute函式中會呼叫
  3. 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}
複製程式碼