1. 程式人生 > >重拾Java--功底篇之HashMap

重拾Java--功底篇之HashMap

最近靠了些關於HashMap的相關內容,覺得有必要梳理一下了。

1、HashMap概述

HashMap是常用的一個集合類,它是基於雜湊表的Map介面的非同步實現。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保證對映的順序,特別是它不保證該順序恆久不變。

2、HashMap資料結構

HashMap實際上是一個“連結串列雜湊”的資料結構,即陣列和連結串列的結合,如下圖
這裡寫圖片描述
在JDK1.7中的程式碼

transient Entry[] table;  

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

可以看出,HashMap中維護了一個Entry為元素的table,transient修飾表示不參與序列化。每個Entry元素儲存了指向下一個元素的引用,構成了連結串列。
在JDK1.8中,用Node代替了Entry。實現類似

transient Node<K,V>[] table;
static class Node<K,V> implements
Map.Entry<K,V> {
final int hash; final K key; V value; Node<K,V> next; ... }

3、HashMap常用方法

1)put方法實現

JDK1.7中的程式碼

public V put(K key, V value) {  
    // HashMap允許存放null鍵和null值。  
    // 當key為null時,呼叫putForNullKey方法,將value放置在陣列第一個位置。  
    if (key == null)  
        return
putForNullKey(value); // 根據key的keyCode重新計算hash值。 int hash = hash(key.hashCode()); // 搜尋指定hash值在對應table中的索引。 int i = indexFor(hash, table.length); // 如果 i 索引處的 Entry 不為 null,通過迴圈不斷遍歷 e 元素的下一個元素。 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; } } // 如果i索引處的Entry為null,表明此處還沒有Entry。 modCount++; // 將key、value新增到i索引處。 addEntry(hash, key, value, i); return null; }

從原始碼可以看出,大致過程是,當我們向HashMap中put一個元素時,首先判斷key是否為null,不為null則根據key的hashCode,重新獲得hash值,根據hash值通過indexFor方法獲取元素對應雜湊桶的索引,遍歷雜湊桶中的元素,如果存在元素與key的hash值相同以及key相同,則更新原entry的value值;如果不存在相同的key,則將新元素從頭部插入。如果陣列該位置上沒有元素,就直接將該元素放到此陣列中的該位置上。

看一下重hash的方法:

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

此演算法加入了高位計算,防止低位不變,高位變化時,造成的hash衝突。

在hashmap中,我們希望元素儘可能的離散均勻的分佈到每一個hash桶中,因此,這邊給出了一個indexFor方法:

static int indexFor(int h, int length) {  
    return h & (length-1);  
}  

這段程式碼使用 & 運算代替取模,效率更高。

再來看一眼addEntry方法,

void addEntry(int hash, K key, V value, int bucketIndex) {  
    // 獲取指定 bucketIndex 索引處的 Entry   
    Entry<K,V> e = table[bucketIndex];  
    // 將新建立的 Entry 放入 bucketIndex 索引處,並讓新的 Entry 指向原來的 Entry  
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);  
    // 如果 Map 中的 key-value 對的數量超過了極限  
    if (size++ >= threshold)  
    // 把 table 物件的長度擴充到原來的2倍。  
        resize(2 * table.length);  
} 

很明顯,這邊程式碼做的事情就是從頭插入新元素;如果size超過了閾值threshold,就呼叫resize方法擴容兩倍。

JDK1.8中的實現

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //onlyIfAbsent用於控制是否有權修改存在的key的值
        //evict 該表是否再建立模式
        // 初始化tab用於儲存原table引用,n為table長度,i為key所在的hash索引,p為tab[i]處的第一個Node或null
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        // HashMap初始化,預設大小是16
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 如果雜湊桶內沒有Node節點,直接新增
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //e用作遍歷
            Node<K,V> e; K k;
            //如果第一個Node與key的hash值相等且key相等,將p的引用給e,用於之後修改
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //紅黑樹、jdk1.8新增
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            //遍歷連結串列
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

jdk1.8中新增了紅黑樹,大致過程類似,不過,jdk1.8中的hash方法做了修改

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

2)get方法實現

jdk1.7中的實現

public V get(Object key) {  
    if (key == null)  
        return getForNullKey();  
    int hash = hash(key.hashCode());  
    for (Entry<K,V> e = table[indexFor(hash, table.length)];  
        e != null;  
        e = e.next) {  
        Object k;  
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
            return e.value;  
    }  
    return null;  
}  

這段程式碼很容易理解,首先根據key的hashCode計算hash值,根據hash值確定桶的位置,然後遍歷。
jdk1.8中的實現

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

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;
    }

jdk1.8中的過程基本類似,值得注意的是,它總是判斷是否為第一個節點,如果不是才進行遍歷,提高了程式碼執行效率

3)resize方法實現

resize 顧名思義就是擴容的意思,那麼,問題來了
HashMap是怎麼擴容的?
擴容多少?
已有元素怎麼處理的?

jdk1.7中,resize()方法在當size 超過 threshold(閾值)= loadFactor 預設0.75 *capacity 時觸發,擴容大小為oldCap<<1,即原容量的兩倍

JDK1.7中的程式碼

    void resize(int newCapacity) {   //傳入新的容量
        Entry[] oldTable = table;    //引用擴容前的Entry陣列
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {  //擴容前的陣列大小如果已經達到最大(2^30)了
            threshold = Integer.MAX_VALUE; //修改閾值為int的最大值(2^31-1),這樣以後就不會擴容了
            return;
        }

        Entry[] newTable = new Entry[newCapacity];  //初始化一個新的Entry陣列
        transfer(newTable);                         //!!將資料轉移到新的Entry數組裡
        table = newTable;                           //HashMap的table屬性引用新的Entry陣列
        threshold = (int) (newCapacity * loadFactor);//修改閾值
    }

這邊程式碼還是很好理解的,方法的核心是transfer,這裡實現了元素的轉移,直接看程式碼

void transfer(Entry[] newTable) {  
    Entry[] src = table;                   //src引用了舊的Entry陣列  
    int newCapacity = newTable.length;  
    for (int j = 0; j < src.length; j++) { //遍歷舊的Entry陣列  
        Entry<K, V> e = src[j];             //取得舊Entry陣列的每個元素  
        if (e != null) {  
            src[j] = null;//釋放舊Entry陣列的物件引用(for迴圈後,舊的Entry陣列不再引用任何物件)  
            do {  
                Entry<K, V> next = e.next;  
                int i = indexFor(e.hash, newCapacity); //!!重新計算每個元素在陣列中的位置  
                e.next = newTable[i]; //標記[1]  
                newTable[i] = e;      //將元素放在陣列上  
                e = next;             //訪問下一個Entry鏈上的元素  
            } while (e != null);  
        }  
    }  
}  

大致過程是:src儲存舊的陣列引用,遍歷src,取得第一個Entry元素e=src[j],釋放src[j]位置的引用,然後遍歷連結串列,用next儲存e.next的引用,對於連結串列的每個元素,都通過indexFor重新找到桶的位置i,將新索引第一個位置的引用給e.next,然後再把e的引用放到桶第一個元素位置,即實現了從頭插入的效果。
乍一看,哇,還不錯耶,可是,其實這段程式碼會導致一個問題,如果一個連結串列上所有元素,重hash之後的位置和原先位置相同,那麼,這個連結串列的順序就會顛倒。

那怎麼辦呢?觀察jdk1.7中,雜湊桶位置計算的方法h & (length-1);,h為key的hash值,當resize觸發時,length變成了oldLength<<1,即向左移動了一位,length-1實際上就是在高位多了1,與hash進行&運算後,實際上除最高位可能會有0,1的變化外,低位是不變的。可以理解為,有些元素擴容後,雜湊桶的位置不變,而有些元素則變為原來位置i+oldLength處的雜湊桶了。如果不懂,可以看以下兩張圖:
這裡寫圖片描述
這裡寫圖片描述

為此,jdk1.8的resize程式碼進行了改進:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            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];
        table = newTab;
        if (oldTab != null) {
            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)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        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;
                        }
                    }
                }
            }
        }
        return newTab;
    }

看到這麼長段原始碼,內心是崩潰的,什麼鬼啊。。這程式碼好長好長啊!!
我們來簡單分析一下過程:
1、前面一段其實是用來初始化以及超過最大閾值時的操作,擴容操作和jdk1.7類似,可以簡略看下
2、從第30行開始為元素轉移方法,首先遍歷oldTab,若雜湊桶記憶體在元素,即e=oldTab[j]!==null,則需要轉移,先將oldTab[j]的引用置為null
3、當雜湊桶記憶體在一個元素的情況下,即e.next==null,使用和jdk1.7一樣的方法將e放到新的table中,雜湊桶位置獲取方法為e.hash & (newCap - 1)
4、若e是紅黑樹、則進行紅黑樹的split操作,這暫時不講,因為不太懂- -
5、40行開始為連結串列多元素情況時的轉移操作,定義了loHead,hiHead用來儲存低位與高位連結串列的頭結點,loTail,hiTail用來儲存低位與高位連結串列的尾結點,這邊高位和低位指的是需要換雜湊索引以及不需要換雜湊索引的情況。next存下一個操作的節點引用。
6、如何判斷是否需要換雜湊索引?這邊使用了一個很巧妙的方法,e.hash & oldCap方法,仔細想一個,因為oldCap是2的倍數,所以在對應的高位處是1,其餘都是0,與hash做&操作後,如果 == 0 說明 不需要換雜湊桶索引。
7、之後的操作便是,若頭結點為null,則將e的引用給頭結點,同時將e的引用給尾結點;之後若不變換則將e的引用給loTail的next,(其實同時修改了loTail的引用的next),並且將e的引用給loTail,這時候loTail又指向lo連結串列的尾結點;hi連結串列同樣操作。
8、將loHead引用給newTab[j],hiHead給newTab[j+oldCap],完成一個連結串列轉移

來看一下買家秀。。
這裡寫圖片描述
好了,貼了太多原始碼,有點累了。。其他方法待續。。

相關推薦

Java--功底HashMap

最近靠了些關於HashMap的相關內容,覺得有必要梳理一下了。 1、HashMap概述 HashMap是常用的一個集合類,它是基於雜湊表的Map介面的非同步實現。此實現提供所有可選的對映操作,並允許使用null值和null鍵。此類不保證對映的順序,特別是

【朝花夕】記憶體(三)Java GC

        在上一篇日誌中有講到,JVM記憶體由程式計數器、虛擬機器棧、本地方法棧、GC堆,方法區五個部分組成。其中GC堆是一塊多執行緒的共享區域,它存在的作用就是存放物件例項。本節中所要講述的各種場景,就發生在這塊區域,垃圾回收也主要發生在GC堆記憶體中。本章內容為高質量面試中幾乎是必問的知識點,尤其是

JavaLinkedList原始碼閱讀

     上文我們檢視ArrayList的原始碼(重拾Java之ArrayList原始碼閱讀),接著我們來瞅瞅LinkedList有什麼神奇之處。ArrayList的資料儲存方式是陣列,LinkedList裡面儲存資料的方式是連結串列,什麼是連結串列了?你可以將其理解為一列火

java基礎nio與aio

sre 非阻塞 只有一個 accept ava 使用步驟 截取 city writable 1.同步和異步 同步:指一個任務運行完以後接著運行下一個任務 異步:接到一個任務後開啟一個新的線程運行此任務,基本不占用原線程時間 2.阻塞與非阻塞 阻塞:多個線程同時訪問一份數據時

2基本類型數組和枚舉類型——Java

arraycopy 字符 第一個 system.in abs xtend 劃線 ann else 2.1 標識符和關鍵字 2.1.1標識符 標識符:用來標誌類名、變量名、方法名、類型名、數組名、文件名的有效字符序列稱為標識符。簡單地說,標識符就是一個名字。 Java關於標識

java集合系列HashMap源碼

實現 幫助 成員變量 eno dea after 一次 == 處的 java集合系列之HashMap源碼   HashMap的源碼可真不好消化!!!   首先簡單介紹一下HashMap集合的特點。HashMap存放鍵值對,鍵值對封裝在Node(代碼如下,比較簡單,不再介紹)

java面試題----HashMap常見面試題總結

使用 這一 hashtable 此刻 獲取 重要性 microsoft ria 取值 “你用過HashMap嗎?” “什麽是HashMap?你為什麽用到它?” 幾乎每個人都會回答“是的”,然後回答HashMap的一些特性,譬如HashMap可以接受null鍵值和值,而Has

Java基礎常量、變數、運算子

資料型別 : Java中的基本型別功能簡單,不具備物件的特性,為了使基本型別具備物件的特性,所以出現了包裝類,就可以像操作物件一樣操作基本型別資料。 基本型別對應的包裝類 基本型別 byte int short long float double boolean char 包裝型別

java原始碼解讀HashMap

1:首先下載openjdk(http://pan.baidu.com/s/1dFMZXg1),把原始碼匯入eclipse,以便看到jdk原始碼            Windows-Prefe

Java基礎順序、選擇、迴圈結構

程式流程  java程式語句執行的順序包括4種基本控制結構:順序結構、選擇結構、迴圈結構、異常處理邏輯結構。 順序結構  Java程式中,語句執行的基本順序按各語句出現的位置先後順序執行,即為順序結構。 例1:順序結構:已知三角形三邊,求三角形面積: pub

JAVA集合學習HashMap

1.程式碼示例   public class Text { public static void main(String[] args) { // TODO Auto-generated method stub Map<String,String> text = ne

java基礎-----------抽象類 、最終類、介面

一、抽象類: 我們把一類事物抽象出來,類中只宣告方法,不實現,這就是抽象類存在的意義; 抽象類: 含有抽象方法的類,被abstract 關鍵字修飾; 抽象方法:只有方法宣告沒有方法實體,被abstract關鍵字修飾; 注意要點: 1、抽象類沒有例項物件,只能通過別的類繼承實現抽象方法

JAVA原始碼分析HashMap

前言 從事了好長時間的開發工作,平時只注重業務程式碼的開發,而忽略了java本身一些基礎,所以從現在開始閱讀以下jdk的原始碼,首先從集合開始吧!這一篇先看下HashMap的原始碼。 java集合架構   &nbs

Java併發程式設計 HashMap執行緒不安全

我想在平時的多執行緒程式設計中,容器的使用是很普遍的,但是你有沒有考慮過有些容器是不安全的,如Haspmap、ArrayList。這裡講解一下Hashmap不安去體現在哪裡。 插入時不安全: 如果有兩個執行緒A和B,都進行插入資料,剛好經過雜湊計算後得到的雜湊碼是一樣的,即插入的

Java集合框架HashMap的原始碼解析

1.首先看一下HashMap的繼承關係 java.lang.Object ↳ java.util.AbstractMap<K, V> ↳ java.util.HashMap<K, V> pub

Java第一波 介面有什麼用

      畢業以後工作了一年,本來不想從事程式設計工作,畢竟這個變禿就能變強的職業有點嚇人。但現在發現還是這個行業才能稍微在現今的物質生活中過得比較舒適,所以開始拿起放下一年的專業---軟體工程。       一開始也在前端,java,c++三個方向猶豫了很久。但是最後還

Java基礎環境搭建

伺服器環境:Centos jdk安裝包:.tar.gz 第一步:在官網下載JDK安裝包 第二步:解壓縮安裝包:tar -xzvf jdk-8u131-linux-x64.tar.gz -C /usr/local/java ps:壓縮到/usr/local/java目錄

java基礎GC

概述 java和C++有著一堵 記憶體動態分配 和 垃圾收集技術 圍成的“高牆”,外面的人想進去,裡面的人想出來。 java垃圾回收 GC(Garbage Collection) 的歷史比java還要久遠,1960年誕生的Lisp語言當時就在考慮三個問題:

Java學習型別的轉化

2018年11月02日 20:34:21 zlemperor 閱讀數:2 標籤: Java 基礎

Java學習如何實現將日期加一天

主要是通過Calendar來完成 package org.tarena.test; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; public c