學習計劃 HashMap(1.7)

1.7的HashMap完整實現了哈希表,哈希表是一種根據鍵值(Key-Value)訪問數據的結構,實現這種結構需要解決兩個問題:

一. 哈希函數

理想的哈希函數對於不同的輸入應該產生不同的結構,同時散列結果應當具有同一性(輸出值儘量均勻)和雪崩效應(微小的輸入值變化使得輸出值發生巨大大變化)
1.7 hash函數實現:

    final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
		//異或運算 相同返回0
        h ^= k.hashCode();
		//1.8改爲高位參與運算 (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }
二. 衝突解決

衝突指的哈希函數計算出的訪問地址已存在數據,均勻的哈希函數可以減少衝突,但不能避免衝突,發生衝突後,必須解決;也即必須尋找下一個可用地址
HashMap中使用拉鍊法來解決衝突, 將所有位置重複的數據使用單項鍊表存儲,也就是數組加鏈表,HashMap使用嵌套類Entry存儲元素,它包含四個屬性:key,value,hash值和用於單向鏈表的next

   static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;

下面是代碼解析,建議對照着代碼看

初始化

HashMap在使用put方法時纔會創建Entry對象,同時他的初始大小大於等於2的冪次方,以7和9爲例
大於等於7的2的冪次方爲8
大於等於9的2的冪次方爲16

put方法判斷Entry
   if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }

threshold 是你傳入的大小,如果沒有傳入,默認爲16:
在這裏插入圖片描述

inflateTable
  private void inflateTable(int toSize) {
        // Find a power of 2 >= toSize 查找一個大於等於toSize的2的冪次方數
        int capacity = roundUpToPowerOf2(toSize);
		//擴容閾值計算 
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //創建 capacity 大小的Entry 實例
        table = new Entry[capacity];
        //rehash 擴容之後需要重新計算hash
        initHashSeedAsNeeded(capacity);
    }

可以看到 new Entry[capacity] ,創建對象時,並沒有使用我們傳入的toSize,而是將它傳入了roundUpToPowerOf2 這個方法

roundUpToPowerOf2
    private static int roundUpToPowerOf2(int number) {
        // assert number >= 0 : "number must be non-negative";
        return number >= MAXIMUM_CAPACITY
                ? MAXIMUM_CAPACITY
                : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
    }

關鍵點在於
Integer.highestOneBit((number - 1) << 1)
這段代碼返回一個小於等於number的二的冪次方,並將他左移一位

highestOneBit
    public static int highestOneBit(int i) {
        // HD, Figure 3-1
        i |= (i >>  1);
        i |= (i >>  2);
        i |= (i >>  4);
        i |= (i >>  8);
        i |= (i >> 16);
        return i - (i >>> 1);
    }

看不懂?沒關係,先打印一下:
在這裏插入圖片描述
下面我以i = 15爲例,推算這個過程

第一行代碼 i |= (i >> 1)

i >> 1 = 15右移一位(將值轉換爲2進制,正數時左邊補0,最後一位去除) ,計算過程:
15=0000 1111(這裏簡單寫爲8位,實際上應該是32位,因爲int是4個字節,每個字節佔8位)
右移一位=0000 0111
然後將兩個值進行 按位或(有一爲一)運算
0000 1111
0000 0111
結果爲:
0000 1111
此時i = 0000 1111 也就是 15

第二行代碼 i |= (i >> 2)

i >> 2 = 15右移兩位,計算過程
15=0000 1111
右移兩位(最後兩位去除,最左邊補零)=0000 0011
按位或運算:
0000 1111
0000 0011
結果爲:
0000 1111
也就是15

第三行代碼 i |= (i >> 4)

15右移四位(最後四位去除),計算過程
15=0000 1111
右移四位=0000 0000
按位或運算:
0000 1111
0000 0000
結果還是0000 1111
至此發現後面的位移運算已經沒有意義了,所以直接跳到最後一行代碼

返回結果 return i - (i >>> 1)

i >>> 1 = 15無符號右移(不論正負,直接補零)一位
15=0000 1111
無符號右移= 000 0111
然後再相減,結果爲:0000 1000
轉換爲10進製爲8

這裏有個技巧叫做8421 從高到低對應不同的權重,舉例:
0000 1111 = 1* 8 + 1* 4 + 1* 2 + 1* 1 = 15
0000 0111 = 0* 8 + 1* 4 + 1* 2 + 1* 1 = 7
0000 1000 = 1* 8 +0* 4 + 0* 2 + 0* 1 = 8

最後highestOneBit(15)這個方法返回值爲 8
我在開頭寫過 :HashMap初始大小大於等於2的冪次方,8顯然不是大於等於15的2的冪次方
我們返回roundUpToPowerOf2方法,查看這段代碼:
在這裏插入圖片描述
它將返回值 8 左移1位
8:0000 1000
左移一位(左邊去除一位,右邊補一位0):0001 0000
轉換爲10進製爲: 16

到這一步就成功的創建了一個大小爲16的HashMap!

Integer.highestOneBit((number - 1) 爲什麼要減一?

我們知道 HashMap的默認大小爲1 << 4 也就是16
highestOneBit 會返回小於等於輸入值的二的冪次方,傳入16會返回16,
此時將16左移一位時會返回32,這樣就會導致創建錯誤大小的HashMap

Put
    public V put(K key, V value) {
        if (table == EMPTY_TABLE) {
            inflateTable(threshold); //初始化
        }
        if (key == null) 
            return putForNullKey(value);//key等於null 時,進入這個方法
        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;
                e.recordAccess(this);
                return oldValue;//如果這個key已存在,返回老的value
            }
        }

        modCount++; //增加修改次數
        addEntry(hash, key, value, i); //添加數據
        return null;
    }
putForNullKey
    private V putForNullKey(V value) {
    //判斷第一個key是否等於null
        for (Entry<K,V> e = table[0]; e != null; e = e.next) {
            if (e.key == null) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;//覆蓋老的value  ,並返回
            }
        }
        modCount++;
        addEntry(0, null, value, 0);
        return null;
    }
indexFor(hash, table.length)
    static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) ==
         1 : "length must be a non-zero power of 2";
        return h & (length-1);
    }

這一步叫做定位哈希桶索引
h & (length-1) 等價於h%table.length

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章