HashMap、Hashtable、LinkedHashMap

目錄:

  1)HashMap

  2)Hashtable

  3)LinkedHashMap

 

 

1)HashMap

 

1、關於哈希表:

在討論哈希表以前,咱們先大概瞭解下其餘數據結構在新增,查找等基礎操做執行性能html

  數組:採用一段連續的存儲單元來存儲數據。對於指定下標的查找,時間複雜度爲O(1);經過給定值進行查找,須要遍歷數組,逐一比對給定關鍵字和數組元素,時間複雜度爲O(n),固然,對於有序數組,則可採用二分查找,插值查找,斐波那契查找等方式,可將查找複雜度提升爲O(logn);對於通常的插入刪除操做,涉及到數組元素的移動,其平均複雜度也爲O(n)java

  線性鏈表:對於鏈表的新增,刪除等操做(在找到指定操做位置後),僅需處理結點間的引用便可,時間複雜度爲O(1),而查找操做須要遍歷鏈表逐一進行比對,複雜度爲O(n)node

  二叉樹:對一棵相對平衡的有序二叉樹,對其進行插入,查找,刪除等操做,平均複雜度均爲O(logn)。面試

  哈希表:相比上述幾種數據結構,在哈希表中進行添加,刪除,查找等操做,性能十分之高,不考慮哈希衝突的狀況下,僅需一次定位便可完成,時間複雜度爲O(1),接下來咱們就來看看哈希表是如何實現達到驚豔的常數階O(1)的。數組

  咱們知道,數據結構的物理存儲結構只有兩種:順序存儲結構鏈式存儲結構(像棧,隊列,樹,圖等是從邏輯結構去抽象的,映射到內存中,也這兩種物理組織形式),而在上面咱們提到過,在數組中根據下標查找某個元素,一次定位就能夠達到,哈希表利用了這種特性,哈希表的主幹就是數組安全

  好比咱們要新增或查找某個元素,咱們經過把當前元素的關鍵字 經過某個函數映射到數組中的某個位置,經過數組下標一次定位就可完成操做。
數據結構

        存儲位置 = f(關鍵字)app

  其中,這個函數f通常稱爲哈希函數,這個函數的設計好壞會直接影響到哈希表的優劣。舉個例子,好比咱們要在哈希表中執行插入操做:ide

查找操做同理,先經過哈希函數計算出實際存儲地址,而後從數組中對應地址取出便可。函數

  哈希衝突

  然而萬事無完美,若是兩個不一樣的元素,經過哈希函數得出的實際存儲地址相同怎麼辦?也就是說,當咱們對某個元素進行哈希運算,獲得一個存儲地址,而後要進行插入的時候,發現已經被其餘元素佔用了,其實這就是所謂的哈希衝突,也叫哈希碰撞。前面咱們提到過,哈希函數的設計相當重要,好的哈希函數會盡量地保證 計算簡單和散列地址分佈均勻,可是,咱們須要清楚的是,數組是一塊連續的固定長度的內存空間,再好的哈希函數也不能保證獲得的存儲地址絕對不發生衝突。那麼哈希衝突如何解決呢?哈希衝突的解決方案有多種:開放定址法(發生衝突,繼續尋找下一塊未被佔用的存儲地址),再散列函數法,鏈地址法,而HashMap便是採用了鏈地址法,也就是數組+鏈表的方式。

2、關於hashmap:

1)HashMap是由數組+鏈表+紅黑樹構成的,數組就稱之爲桶了

  HashMap 根據鍵的 hashCode 值存儲數據,大多數狀況下能夠直接定位到它的值,於是具備很快的訪問速度,但遍歷順序倒是不肯定的。 HashMap 最多隻容許一條記錄的鍵爲 null ,容許多條記錄的值爲 null 。HashMap 非線程安全,即任一時刻能夠有多個線程同時寫 HashMap,可能會致使數據的不一致。若是須要知足線程安全,能夠用 Collections的synchronizedMap 方法使 HashMap 具備線程安全的能力,或者使用ConcurrentHashMap ,或者或者其它。。。。。。

 

 

3、源碼

基本指標:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認初始化容量爲16,must be a power of two,詳見下面
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量1G
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默認負載因子
static final int TREEIFY_THRESHOLD = 8;// 從鏈表變爲紅黑樹的閾值,當鏈表長度大於等於8時,由鏈表轉換成紅黑樹
static final int UNTREEIFY_THRESHOLD = 6; // 從紅黑樹變爲鏈表的閾值
// 當須要將解決 hash 衝突的鏈表轉變爲紅黑樹時,須要判斷下此時數組容量,如果因爲數組容量過小(小於 MIN_TREEIFY_CAPACITY )致使的 hash 衝突太多,則不進行鏈表轉變爲紅黑樹操做,轉爲利用 resize() 函數對 hashMap 擴容
// 當某個桶中的鏈表長度達到8進行鏈表扭轉爲紅黑樹的時候,會檢查總桶數是否小於64,若是總桶數小於64也會進行擴容;    
static final int MIN_TREEIFY_CAPACITY = 64;

成員變量:

//好比說,在初始化時,默認的容量是16,那麼table的length就是16,其threshold=容量×負載因子=16×0.75=12,這就表明着,當size大於12時,就會進行擴容(容量會×2,threshold會根據新容量從新計算)的操做!                                         
//這樣作的目的很明確,就是爲了減小哈希衝突!有效元素的個數少於哈希表的總大小時,其產生哈希衝突的可能性必定是小於相等狀況的!                                                                                                      
                                                                                                                                                                     
transient Node<K,V>[] table; // 真正開闢的空間,其length就是真正的容量大小;真正佔用空間(用不用是一回事,先佔用先);When allocated, length is always a power of two.                      
transient int size; // 真正使用的空間;有效的結點個數;總的鍵值對的個數;    The number of key-value mappings contained in this map.                                                          
int threshold; // 閾值,大於這個值,擴容;The next size value at which to resize (capacity * load factor);用來記錄當前容量下,最適合存放多少鍵值對(容量*負載因子)                                          
final float loadFactor; // 負載因子,默認0.75                                                                                                                               
transient int modCount; //用於快速失敗,因爲HashMap非線程安全,在對HashMap進行迭代時,若是期間其餘線程的參與致使HashMap的結構發生變化了(好比put,remove等操做),須要拋出異常ConcurrentModificationException                   
transient Set<Map.Entry<K,V>> entrySet; // //由 hashMap 中 Node<K,V> 節點構成的 set

 

靜態工具:

/**
 * Node<K, V>是一個靜態內部類,封裝了這個結點的全部信息
 */
static class Node<K,V> implements Map.Entry<K,V>{
    final int hash;  // 相對應的hash值,其方法見下
    final K key;
    V value;
    Node<K,V> next;  // 鏈表中指向下一處的指針;爲了解決哈希衝突,當產生哈希衝突時,next就能夠指向一張鏈表,或者一棵黑樹!
  ......
}

/**
 *  計算其hash值的方法,看不懂
 */
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

/**
 *  這樣的設計一切都是爲了性能
 *  當 table.length 知足2的整數次冪時,如下條件成立:
 *  hash & (table.length - 1) == hash % table.length
 */
hash & (table.length - 1)

/**
 * 方法返回的值是最接近 initialCapacity 的2的冪,這些位的設計一切都是爲了性能
 * 若指定初始容量爲9,則返回16,
 * 若指定初始容量爲5,則返回8,
 * @param cap
 * @return
 */
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;
}

 構造方法:

// 賦值閾值以及負載因子初始化
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0) {
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    }
    if (initialCapacity > MAXIMUM_CAPACITY) {
        initialCapacity = MAXIMUM_CAPACITY;
    }
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    }
    this.loadFactor = loadFactor;  // 初始化負載因子
    this.threshold = tableSizeFor(initialCapacity);  // 閾值初始化,最後仍是變成容量的初始化。見:resize() 的 0】、2-1】,這裏說白了就只是一層迷惑人的轉換罷了
}

// 默認負載因子爲0.75了
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
 * 最經常使用的hashMap的構造器
 * 這裏只賦值了負載因子哦
 * 閾值以及容量是在put的時候搞進去的,位於 resize()方法的 4】
 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

 put():

1)若是存在Hash碰撞就會以鏈表的形式保存,把當前傳進來的參數生成一個新的節點保存在鏈表的尾部(JDK1.7保存在首部)。而若是鏈表的長度大於8那麼就會以紅黑樹的形式進行保存(位於 3-3-3-1】)。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; // 這裏的tab就是指這個table,只不過不用table罷了
    Node<K,V> p; //指hashMap裏面的table數組下標的node值 p=table[i] (就是指那個槽)
    int n;//指hashMap裏面的table數組長度
    int i;//指hashMap裏面的table數組下標(就是那個槽的下標)
    // 1】若是table未初始化或長度爲0,則進行初始化(馬上擴容)(hashMap的最經常使用的構造函數第一次初始化就是在這裏)
    if ((tab = table) == null || (n = tab.length) == 0) {
        n = (tab = resize()).length;// 1-1】初始化的時候,調用resize()方法,獲得hashmap裏面的數組長度。空參構造後的put()方法的閾值以及容量都是在resize()獲得的
    }
    // 2】若是節點hash值對應的數組位置爲空,直接賦值
    if ((p = tab[i = (n - 1) & hash]) == null) {// i = (n - 1) & hash 求hashmap數組下標並賦值給i,判斷相對應的數組節點是否爲空
        tab[i] = newNode(hash, key, value, null);  // 若是爲空,直接增長一個節點,很簡單
    } else {  //3】若是不爲空的話
        Node<K,V> e; // 對應下標的新的節點的node值
        K k;//相對應下標的node值p的key
        //若是hash值同樣,key值也同樣(注意,由於hash值同樣,key值可能不同;先對比hash值再對比key值的(由於對比hash值速度更快)),則直接替換。key值同樣的操做
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {  // 3-1】key 值同樣時怎麼處理(鏈表長度爲1)
            e = p;
        } else if (p instanceof TreeNode) { // 3-2】判斷節點是否爲樹節點,若是是,則按紅黑樹的插入方式插入元素
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        } else {  // 2-3】若是不是樹節點,則按鏈表的方式插入元素(由於這個槽是非空的,這裏的for循環就是對這些槽進行遍歷以處理)(鏈表長度大於1)
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {  // 3-3-1】下一個節點恆等於空,說明是放進鏈表的末尾嘛
                    p.next = newNode(hash, key, value, null); //3-3-1】在末尾放這個node值嘛
                    if (binCount >= TREEIFY_THRESHOLD - 1) // 3-3-1-1】若是這個條件成立,說明最後的尾節點已是7了,要樹化了,這個方法名很是形象
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {  // 若是在這個鏈表上,key和value都同樣的話
                    break;
                }
                p = e;
            }
        }
        //改變value值
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null) {//註釋裏:if true, don't change existing value,可是put這方方法傳過來的是false
                e.value = value;
            }
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;

    //若是大於閾值,則擴容
    if (++size > threshold) {
        resize();
    }
    afterNodeInsertion(evict);
    return null;
}

resize():

1) 該方法會在HashMap的鍵值對達到「閾值」後進行數組擴容,而擴容時會調用resize()方法,此外,在jdk1.7中數組的容量是在HashMap初始化的時候就已經賦予,而在jdk1.8中是在put第一個元素的時候纔會賦予數組容量,而put第一個元素的時候也會調用resize()方法。\

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;// 舊數組賦值給oldTab,表明擴容以前HashMap中的數組,也就是全部的舊桶,舊容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;//oldCap是指oldCapacity,表明擴容以前總桶數量
    int oldThr = threshold;// 0】舊閾值
    int newCap; //新容量,此次擴容以後總桶數量
    int newThr = 0;//新閾值
    if (oldCap > 0) { // 1】table擴容過
        if (oldCap >= MAXIMUM_CAPACITY) { //若是超過最大容量,就再也不擴容,注意threshold = Integer.MAX_VALUE;
            threshold = Integer.MAX_VALUE;
            return oldTab;
        } else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {// 若是老容量擴大2倍仍不超過最大值,則新容量爲原來的2倍
            newThr = oldThr << 1; // double threshold
        }
    } else if (oldThr > 0) {// 2】initial capacity was placed in threshold;使用帶有初始容量的構造器時走這裏,table容量爲初始化獲得的threshold(多麼巧妙的設計啊)
        newCap = oldThr; // 2-1】
    } else { // 3】zero initial threshold signifies using defaults;new HashMap().put("",""):默認構造器走這裏
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) { // 4】不帶有初始容量的構造器走這裏
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr; // 終於獲得新閾值了啊可貴
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 就是在這裏真正對HashMap裏面的數組進行初始化的
    table = newTab;  // 若是一開始是new hashmap(),則不走下面這一步了,由於oldTal爲空嘛
    if (oldTab != null) {         //對新擴容後的table進行賦值
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;// 相對應的下標值
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;  // 將老數組元素置空,讓垃圾回收器回收
                if (e.next == null) {   // 若是數組元素沒有鏈表,直接添加到新數組
                    newTab[e.hash & (newCap - 1)] = e;
                } else if (e instanceof TreeNode) {  // 若是e是樹節點,則按照樹結構處理該分支
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                } else { // preserve order 若是e是鏈表節點,則按照鏈表結構處理該分支
                    Node<K,V> loHead = null, loTail = null;//此對象接收會放在原來位置
                    Node<K,V> hiHead = null, hiTail = null;//此對象接收會放在「j + oldCap」(當前位置索引+原容量的值)
                    Node<K,V> next;
                    do {    // 這個do while 循環就是遍歷鏈表的
                        next = e.next;
                        // 這個判斷是個精華,就是判斷rehash是否須要移位:詳見參考文件第四篇,下面有說
                        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;
                    }
                }
            }
        }
    }
    return newTab;
}

 

 get方法:

final Node<K,V> getNode(int hash, Object key) {                                                                           
    Node<K,V>[] tab; // table副本                                                                                           
    Node<K,V> first; // 相對應下標的那個數組                                                                                        
    Node<K,V> e;                                                                                                          
    int n; // table的數組長度                                                                                                  
    K k;                                                                                                                  
    // table必定不能大於0啊,不然就返回空啊                                                                                              
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {                         
     // always check first node  若是hash值同樣,並且key值也同樣;高度注意:桶中第一項(數組元素)相等(是桶中第一項元素,是第一項):第一項特別判斷,由於鏈表紅黑樹不影響                 
        if (first.hash == hash && ((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;                                                                                                          
}            
View Code

2)Hashtable

主要是看其區別 ,在xmind上面看吧

3)LinkedHashMap

參考資料

  6)JDK1.8源碼(九)——java.util.LinkedHashMap 類

 

參考連接:

1)HashMap實現原理及源碼分析

2)深刻理解HashMap的擴容機制從這裏能夠了解到,jdk7的擴容標準跟jdk8是不同的

3)JDK1.8源碼(七)——java.util.HashMap 類   寫得真的很用心

4)jdk8之HashMap resize方法詳解(深刻講解爲何1.8中擴容後的元素新位置爲原位置+原數組長度):很是感謝這個做者,終於看明白hahsmap的擴容了

5)面試:(1)美團面試題:Hashmap的結構,1.7和1.8有哪些區別,史上最深刻的分析    

6)JDK1.8源碼(九)——java.util.LinkedHashMap 類

相關文章
相關標籤/搜索