1. 程式人生 > >hashMap詳解與例項

hashMap詳解與例項

在Java集合類中最常用的除了ArrayList外,就是HashMap了。本文儘自己所能,儘量詳細的解釋HashMap的原始碼。一山還有一山高,有不足之處請之處,定感謝指定並及時修正。

    在看HashMap原始碼之前先複習一下資料結構。

    Java最基本的資料結構有陣列和連結串列。陣列的特點是空間連續(大小固定)、定址迅速,但是插入和刪除時需要移動元素,所以查詢快,增加刪除慢。連結串列恰好相反,可動態增加或減少空間以適應新增和刪除元素,但查詢時只能順著一個個節點查詢,所以增加刪除快,查詢慢。有沒有一種結構綜合了陣列和連結串列的優點呢?當然有,那就是雜湊表(雖說是綜合優點,但實際上查詢肯定沒有陣列快,插入刪除沒有連結串列快,一種折中的方式吧)。一般採用拉鍊法實現雜湊表。雜湊表?拉鍊法?可能一下想不起來,不過放張圖就瞭然了。

    (圖片google來的,好多都用了文章用了這張圖了,不知道出處了就沒申明作者了)

    學計算機的肯定學過這玩意兒,也不需要解釋,都懂的。

    鋪墊了這麼多,又是陣列又是連結串列,還有雜湊表,拉鍊法,該入正題了,我們什麼時候用到了這些內容,具體它是怎麼實現的?

    其實我們一直在用(別告訴我你沒用過HashMap什麼的),可能你一直沒去深究,沒看到它如何實現的,所以一直沒感受到。這裡主要分析HashMap的原始碼,就不再多扯其他的了。

    HashMap繼承自AbstractMap,實現了Map介面(這些內容可以參考《Java集合類》

)。來看類的定義。

1 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable

    Map介面定義了所有Map子類必須實現的方法。Map介面中還定義了一個內部介面Entry(為什麼要弄成內部介面?改天還要學習學習)。Entry將在後面有詳細的介紹。

    AbstractMap也實現了Map介面,並且提供了兩個實現Entry的內部類:SimpleEntry和SimpleImmutableEntry。

    定義了介面,介面中又有內部介面,然後有搞了個抽象類實現介面,抽象類裡面又搞了兩個內部類實現介面的內部介面,有沒有點繞,為什麼搞成這樣呢?先不管了,先看HashMap吧。

    HashMap中定義的屬性(應該都能看明白,不過還是解釋一下):

複製程式碼
 1 /**
 2      * 預設的初始容量,必須是2的冪。
 3      */
 4     static final int DEFAULT_INITIAL_CAPACITY = 16;
 5     /**
 6      * 最大容量(必須是2的冪且小於2的30次方,傳入容量過大將被這個值替換)
 7      */
 8     static final int MAXIMUM_CAPACITY = 1 << 30;
 9     /**
10      * 預設裝載因子,這個後面會做解釋
11      */
12     static final float DEFAULT_LOAD_FACTOR = 0.75f;
13     /**
14      * 儲存資料的Entry陣列,長度是2的冪。看到陣列的內容了,接著看陣列中存的內容就明白為什麼博文開頭先複習資料結構了
15      */
16     transient Entry[] table;
17     /**
18      * map中儲存的鍵值對的數量
19      */
20     transient int size;
21     /**
22      * 需要調整大小的極限值(容量*裝載因子)
23      */
24     int threshold;
25     /**
26      *裝載因子
27      */
28     final float loadFactor;
29     /**
30      * map結構被改變的次數
31      */
32     transient volatile int modCount;
複製程式碼

    接著是HashMap的構造方法。

複製程式碼
/**
     *使用預設的容量及裝載因子構造一個空的HashMap
     */
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);//計算下次需要調整大小的極限值
        table = new Entry[DEFAULT_INITIAL_CAPACITY];//根據預設容量(16)初始化table
        init();
    }
/**
     * 根據給定的初始容量的裝載因子建立一個空的HashMap
     * 初始容量小於0或裝載因子小於等於0將報異常 
     */
    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);
        int capacity = 1;
        //設定capacity為大於initialCapacity且是2的冪的最小值
        while (capacity < initialCapacity)
            capacity <<= 1;
        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init();
    }
/**
     *根據指定容量建立一個空的HashMap
     */
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);//呼叫上面的構造方法,容量為指定的容量,裝載因子是預設值
    }
/**
     *通過傳入的map建立一個HashMap,容量為預設容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的較大者,裝載因子為預設值
     */
    public HashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
        putAllForCreate(m);
    }
複製程式碼

    上面的構造方法中呼叫到了init()方法,最後一個方法還呼叫了putAllForCreate(Map<? extends K, ? extends V> m)。init方法是一個空方法,裡面沒有任何內容。putAllForCreate看方法名就是建立的時候將傳入的map全部放入新建立的物件中。該方法中還涉及到其他方法,將在後面介紹。

    先看初始化table時均使用了Entry,這是HashMap的一個內部類,實現了Map介面的內部介面Entry。

    下面給出Map.Entry介面及HashMap.Entry類的內容。

    Map.Entry介面定義的方法

1 K getKey();//獲取Key
2 V getValue();//獲取Value
3 V setValue();//設定Value,至於具體返回什麼要看具體實現
4 boolean equals(Object o);//定義equals方法用於判斷兩個Entry是否相同
5 int hashCode();//定義獲取hashCode的方法

    HashMap.Entry類的具體實現

複製程式碼
 1 static class Entry<K,V> implements Map.Entry<K,V> {
 2         final K key;
 3         V value;
 4         Entry<K,V> next;//對下一個節點的引用(看到連結串列的內容,結合定義的Entry陣列,是不是想到了雜湊表的拉鍊法實現?!)
 5         final int hash;//雜湊值
 6 
 7         Entry(int h, K k, V v, Entry<K,V> n) {
 8             value = v;
 9             next = n;
10             key = k;
11             hash = h;
12         }
13 
14         public final K getKey() {
15             return key;
16         }
17 
18         public final V getValue() {
19             return value;
20         }
21 
22         public final V setValue(V newValue) {
23         V oldValue = value;
24             value = newValue;
25             return oldValue;//返回的是之前的Value
26         }
27 
28         public final boolean equals(Object o) {
29             if (!(o instanceof Map.Entry))//先判斷型別是否一致
30                 return false;
31             Map.Entry e = (Map.Entry)o;
32             Object k1 = getKey();
33             Object k2 = e.getKey();
34 // Key相等且Value相等則兩個Entry相等
35             if (k1 == k2 || (k1 != null && k1.equals(k2))) {
36                 Object v1 = getValue();
37                 Object v2 = e.getValue();
38                 if (v1 == v2 || (v1 != null && v1.equals(v2)))
39                     return true;
40             }
41             return false;
42         }
43         // hashCode是Key的hashCode和Value的hashCode的異或的結果
44         public final int hashCode() {
45             return (key==null   ? 0 : key.hashCode()) ^
46                    (value==null ? 0 : value.hashCode());
47         }
48         // 重寫toString方法,是輸出更清晰
49         public final String toString() {
50             return getKey() + "=" + getValue();
51         }
52 
53         /**
54          *當呼叫put(k,v)方法存入鍵值對時,如果k已經存在,則該方法被呼叫(為什麼沒有內容?)
55          */
56         void recordAccess(HashMap<K,V> m) {
57         }
58 
59         /**
60          * 當Entry被從HashMap中移除時被呼叫(為什麼沒有內容?)
61          */
62         void recordRemoval(HashMap<K,V> m) {
63         }
64     }
複製程式碼

    看完屬性和構造方法,接著看HashMap中的其他方法,一個個分析,從最常用的put和get說起吧。

    put()

複製程式碼
 1 public V put(K key, V value) {
 2         if (key == null)
 3             return putForNullKey(value);
 4         int hash = hash(key.hashCode());
 5         int i = indexFor(hash, table.length);
 6         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
 7             Object k;
 8             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
 9                 V oldValue = e.value;
10                 e.value = value;
11                 e.recordAccess(this);
12                 return oldValue;
13             }
14         }
15 
16         modCount++;
17         addEntry(hash, key, value, i);
18         return null;
19     }
複製程式碼

    當存入的key是null的時候將呼叫putForNUllKey方法,暫時將這段邏輯放一邊,看key不為null的情況。先呼叫了hash(int h)方法獲取了一個hash值。

複製程式碼
1 static int hash(int h) {
2         // This function ensures that hashCodes that differ only by
3         // constant multiples at each bit position have a bounded
4         // number of collisions (approximately 8 at default load factor).
5         h ^= (h >>> 20) ^ (h >>> 12);
6         return h ^ (h >>> 7) ^ (h >>> 4);
7     }
複製程式碼

    這個方法的主要作用是防止質量較差的雜湊函式帶來過多的衝突(碰撞)問題。Java中int值佔4個位元組,即32位。根據這32位值進行移位、異或運算得到一個值。

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

    indexFor返回hash值和table陣列長度減1的與運算結果。為什麼使用的是length-1?應為這樣可以保證結果的最大值是length-1,不會產生陣列越界問題。

    獲取索引位置之後做了什麼?探測table[i]所在的連結串列,所發現key值與傳入的key值相同的物件,則替換並返回oldValue。若找不到,則通過addEntry(hash,key,value,i)新增新的物件。來看addEntry(hash,key,value,i)方法。

1 void addEntry(int hash, K key, V value, int bucketIndex) {
2     Entry<K,V> e = table[bucketIndex];
3         table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
4         if (size++ >= threshold)
5             resize(2 * table.length);
6     }

    這就是在一個連結串列頭部插入一個節點的過程。獲取table[i]的物件e,將table[i]的物件修改為新增物件,讓新增物件的next指向e。之後判斷size是否到達了需要擴充table陣列容量的界限並讓size自增1,如果達到了則呼叫resize(int capacity)方法將陣列容量拓展為原來的兩倍。

複製程式碼
 1 void resize(int newCapacity) {
 2         Entry[] oldTable = table;
 3         int oldCapacity = oldTable.length;
 4         // 這個if塊表明,如果容量已經到達允許的最大值,即MAXIMUN_CAPACITY,則不再拓展容量,而將裝載拓展的界限值設為計算機允許的最大值。
 5         // 不會再觸發resize方法,而是不斷的向map中新增內容,即table陣列中的連結串列可以不斷變長,但陣列長度不再改變
 6         if (oldCapacity == MAXIMUM_CAPACITY) {
 7             threshold = Integer.MAX_VALUE;
 8             return;
 9         }
10         
            
           

相關推薦

hashMap例項

在Java集合類中最常用的除了ArrayList外,就是HashMap了。本文儘自己所能,儘量詳細的解釋HashMap的原始碼。一山還有一山高,有不足之處請之處,定感謝指定並及時修正。     在看HashMap原始碼之前先複習一下資料結構。     Ja

log4j.properties配置例項-全部測試通過[轉]

最近使用log4j寫log時候發現網上的寫的都是千篇一律,寫的好的嘛不全,寫的全一點的嘛沒有一點格式,看著累。這裡把網上收集到的整理了一下,並且全部都在機器上測試成功了。這麼好的文件估計沒有了吧?  ###############################################

spark log4j.properties配置例項

################################################################################  #①配置根Logger,其語法為:  #  #log4j.rootLogger = [level],appen

Android控制元件之SlidingDrawer(滑動式抽屜)例項

SlidingDrawer效果想必大家也見到過,它就是1.5模擬器上進入應用程式列表的效果。下面是截圖  一、簡介    SlidingDrawer隱藏屏外的內容,並允許使用者通過handle以顯示隱藏內容。它可以垂直或水平滑動,它有倆個View組成,其

【python】numpy庫陣列拼接np.concatenate官方文件例項

在實踐過程中,會經常遇到陣列拼接的問題,基於numpy庫concatenate是一個非常好用的陣列操作函式。 1、concatenate((a1, a2, …), axis=0)官方文件詳解 concatenate(...) concatenate(

有趣吧公開的一個api,例項

有趣吧公開的一個api介紹顯示有趣新鮮事路徑:http://www.youqubar.com/api/status_[json|xml|rss]引數:count(可選)-記錄數,範圍1-20,預設為20.示例:http://www.youqubar.com/api/statu

Android開發之位置定位例項解析(GPS定位、Google網路定位,BaiduLBS(SDK)定位)

/** * 由經緯度獲取所在的城市及區域資訊 * @author caizhiming * */ private class ReadJSONFeedTask extends AsyncTask<String, Void, String> {

log4j.properties配置例項-全部測試通過

最近使用log4j寫log時候發現網上的寫的都是千篇一律,寫的好的嘛不全,寫的全一點的嘛沒有一點格式,看著累。這裡把網上收集到的整理了一下,並且全部都在機器上測試成功了。這麼好的文件估計沒有了吧? ######################################

@keyframes、transform例項

一、transform 和@keyframes動畫的區別: @keyframes動畫是迴圈的,而transform 只執行一遍. 二、@keyframes CSS3中新增的新屬性animation是用來為元素實現動畫效果的,但是animation無法單獨擔當起實現動畫的效果。承載動畫的另一個屬性——@k

linux下aio非同步讀寫例項

1.為什麼會有非同步I/O aio非同步讀寫是在linux核心2.6之後才正式納入其標準。之所以會增加此模組,是因為眾所周知我們計算機CPU的執行速度遠大於I/O讀寫的執行速度,如果我們用傳統的阻塞式或非阻塞式來操作I/O的話,那麼我們在同一個程式中(不用多執

SCTP協議例項

1.SCTP是什麼? 只要是接觸過程式設計的人,當你問他傳輸層都有哪些協議?我想幾乎很多人會說TCP,IP協議而很少有人知道SCTP(流控制傳輸協議)這個和上述倆個協議具有相同地位的協議。 SCTP提供的服務與TCP,UDP類似,或者甚至可以理解為其是TCP

03 -1 pandas 中 DataFrame理解建立、索引、運算的以及例項

DataFrame DataFrame是一個【表格型】的資料結構,可以看做是【由Series組成的字典】(共用同一個索引)。DataFrame由按一定順序排列的多列資料組成。設計初衷是將Series的使用場景從一維拓展到多維。DataFrame既有行索引,也有列索引。 行索引

HashMap( JDK8 之前之後對比)

HashMap簡介 HashMap 是一個散列表,它儲存的內容是鍵值對(key-value)對映。 HashMap 繼承於AbstractMap,實現了Map、Cloneable、java.io.Serializable介面。 HashMap 的實現不是同步的,這意味著它不是執行緒安全的。它的k

JAVA反射例項介紹

我們都知道Java反射很重要,這次我來拋個磚頭!!! 一:反射    反射是在執行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法和屬性。   使用java的反射,一般有下面三步:     1:獲得你想操作類

python子程序模組subprocess應用例項 之三

二、應用例項解析 2.1 subprocess模組的使用 1. subprocess.call subprocess.call([“ls”, “-l”]) 0 subprocess.call(“ex

python子程序模組subprocess應用例項 之二

1.2. Popen 物件 Popen類的例項有下列方法: 1. Popen.poll() 檢查子程序是否已經結束,設定並返回返回碼值。 2. Popen.wait() 等待子程序結束,設定並返回返回碼值。 WARNING: 當使用 stdout=P

python子程序模組subprocess應用例項

一、subprocess 模組簡介 subprocess最早是在2.4版本中引入的。 subprocess模組用來生成子程序,並可以通過管道連線它們的輸入/輸出/錯誤,以及獲得它們的返回值。 它用來代替多箇舊模組和函式: os.system os.sp

子網劃分子網劃分例項精析

目錄 子網劃分理論基礎 為什麼進行子網劃分 減少網路流量,無論什麼樣的流量,我們都希望它少些,網路流量亦如此。如果沒有可信賴的路由器,網路流量可能導致整個網路停頓,但有了路由器後,大部分流量都將呆在本地網路內,只有前往其他網路的分組將穿越路

一維陣列指標,二維陣列指標,指標陣列及陣列指標的概念例項解析

概念詳解:指標:指標與“int a”,“float b”一樣,也是一種變數,只是指標變數中儲存的是記憶體單元的地址,這是與“int a”和“float b”的本質區別,C語言的精華就在於指標、結構體和連結串列。一維陣列:定義一維陣列之後,即在記憶體中分配一段連續的地址空間,如

route命令使用例項

1.   使用背景 需要接入兩個網路,一個是部署環境所在內網環境,這個環境是上不了外網, 外網環境很可能是一個無線網路。如果兩者都連線上,很可能導致有一方不能起作用,即外網或內網上不了,常常需要使用繁瑣的“禁用網路連線”、“啟用網路連線”的操作來進行內外網的切換,甚是麻