HashMap Java 8 重點內容詳解

從原理和細節上搞定HashMap

聲明:網上講HashMap的帖子很多,各自有各自着重介紹的地方,個人把自己比較感興趣的內容和自己的一點點認識寫下了。
這裏寫圖片描述
hashmap是由數組和鏈表組成的,要插入的元素首先根據哈希函數得到hash值,然後根據規則(取模),得到自己要插入的桶(所謂的桶就是圖中的0-15的數組元素)的號。然後排在桶中元素的後面。而要取的時候也一樣,先拿到桶號,在沿着這個指針逐個往下找。比如上述哈希表中,我們現在假設一個要插入的元素經過Hash函數後的值是44,44%16=12;所以這個元素要插入到桶號爲12的桶中,接下來它一直往下找,最後排在了140後面。
話不多說,基本的原理我想大多數人應該也知道。不清楚的可以去有些博客上看看,應該很快就明白。

然後我們來看看JAVA1.8中關於Hashmap的源碼:
這裏有些不太常用的字段和函數就不一一列舉了。
HashMap主要屬性:

public class HashMap<k,v> extends AbstractMap<k,v> implements Map<k,v>, Cloneable, Serializable {

private static final long serialVersionUID = 362498820763181265L;

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認桶的數量

static final int MAXIMUM_CAPACITY = 1 << 30;//最大容量

static final float DEFAULT_LOAD_FACTOR = 0.75f;//填充比

//當add一個元素到某個位桶,其鏈表長度達到8時將鏈表轉換爲紅黑樹
static final int TREEIFY_THRESHOLD = 8;


static final int UNTREEIFY_THRESHOLD = 6;


static final int MIN_TREEIFY_CAPACITY = 64;

transient Node<k,v>[] table;//存儲元素的數組


transient Set<map.entry<k,v>> entrySet;

transient int size;//存放元素的個數

transient int modCount;//被修改的次數fast-fail機制

int threshold;//臨界值 當實際大小(容量*填充比)超過臨界值時,會進行擴容

final float loadFactor;//填充比

而其中的負載因子loadFactor的理解爲:HashMap中的數據量/HashMap的總容量(initialCapacity),當loadFactor達到指定值或者0.75時候,HashMap的總容量自動擴展一倍。

其中最常用的兩個函數實現大致如下:
// 存儲時:

int hash = key.hashCode(); 
int index = hash % Entry[].length;
Entry[index] = value;

// 取值時:

int hash = key.hashCode();
int index = hash % Entry[].length;
return Entry[index];

接下來我覺得hash函數這點很重要。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

這裏hashCode()通過對象得到一個int的編碼,不同的對象的函數不一樣,如String的hashCode()
就是將String轉化成char數組,然後把這個數組看做是一個32進制的數。

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

至於之後的h = key.hashCode()) ^ (h >>> 16)
開始有些不懂,後面去知乎上看了看。
這段代碼,Java 7是這麼寫的

   static int hash(int h) {
     h ^= (h >>> 20) ^ (h >>> 12);
     return h ^ (h >>> 7) ^ (h >>> 4);
   }

Java 8只做一次16位右位移異或混合,而不是四次,但原理是不變的。兩段代碼的目的都是想讓哈希函數映射得比較均勻鬆散。hash的過程
右唯一16位,正好32位的一半,自己的高半區和低半區做異或,就是爲了混合原始哈希碼的高位和低位,以此來加大低位的隨機性。而且混合後的低位摻雜了高位的部分特徵,這樣高位的信息也被變相的保留下來。

後面還有個人認爲比較重要的內容就是resize(),以及Java 8中的紅黑樹。
內容會在後面補充。
treefiybin這裏寫圖片描述
紅黑樹替代普通的鏈表這裏寫圖片描述

最後來看看java8 很大的一個改進resize 擴容
舉個例子
上邊圖中第0個下標有496和896, 假設它倆的hashcode(int型,佔4個字節)是
resize前:
496的hashcode: 00000000 00000000 00000000 00000000
896的hashcode: 01010000 01100000 10000000 00100000
oldCap是16: 00000000 00000000 00000000 00010000
496和896對應的e.hash & (oldCap)的值爲0, 即下標都是第0個。

resize後:
496的hashcode: 00000000 00000000 00000000 00*0*00000
896的hashcode: 01010000 01100000 10000000 00*1*00000
oldCap是32: 00000000 00000000 00000000 00**1**00000
代碼中 if ((e.hash & oldCap) == 0)
496和896對應的e.hash & oldCap的值爲0和1, 即下標是第0個和第16個。
它將原來的鏈表數據散列到2個下標位置, 概率分別是0.5。
因爲hashcode的第n位是0/1的概率相同, 理論上鍊表的數據會均勻分佈到當前下標或高位數組對應下標。
回顧JDK1.7的HashMap,在擴容時會rehash即每個entry的位置都要再計算一遍, 性能不好, 所以JDK1.8做了這個優化。
再回到文章最開始的問題, HashMap爲什麼用&得到下標,而不是%? 如果使用了取模%, 那麼在容量變爲2倍時, 需要rehash確定每個鏈表元素的位置。

本文好多地方都是在各位大神的博客上摘抄和學習到的,只是自己整理歸納方便以後複習。
具體的引用就不寫了,忘各位博主見諒!

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