由JDK源碼學習HashMap

  HashMap基於hash表的Map接口實現,它實現了Map接口中的全部操做。HashMap容許存儲null鍵和null值。這是它與Hashtable的區別之一(另一個區別是Hashtable是線程安全的)。另外,HashMap中的鍵值對是無序的。下面,咱們從HashMap的源代碼來分析HashMap的實現,如下使用的是Jdk1.7.0_51。java

 1、HashMap的存儲實現算法

  HashMap底層採用的是數組和鏈表這兩種數據結構.當咱們把key-value對put到HashMap時,系統會根據hash算法計算key的hash值,根據hash值決定key-value對存放在數組的哪一個位置(也就是散列表中的」桶」位).若是該位置已經存放Entry,則該位置上的Entry造成Entry鏈.下面咱們從源代碼入手分析.數組

public V put(K key, V value) {
        //① 若是table爲空,調用inflateTable()初始化table數組
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        // 若是key爲null,調用putForNullKey()處理
        if (key == null)
            return putForNullKey(value);
        // ② 調用hash算法,算出key的hash值
        int hash = hash(key);
        // ③ 根據hash值和table的長度計算在table中的存放位置
        int i = indexFor(hash, table.length);
        // 若是key存在,則替換以前的value值
        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;
            }
        }
        // 模數自增,用於實現fail-fast機制
        modCount++;
        // ④ 添加key-value對
        addEntry(hash, key, value, i);
        return null;
}

  上面的程序中用到了一個重要的內部接口:Map.Entry,每一個Map.Entry其實就是一個封裝了key-value屬性的對象.從上面的代碼中也能夠看出:系統決定HashMap中的key-value對時,沒有考慮Entry中的value,僅僅是根據key來計算並決定每一個Entry的存儲位置.安全

  從①處代碼能夠看到,調用put方法時會檢查table數組的容量.若是table數組爲空數組,會先初始化table數組,咱們看下HashMap是如何初始化table數組的。數據結構

private void inflateTable(int toSize) {
        // 找到大於toSize的最小的2的n次方
        int capacity = roundUpToPowerOf2(toSize);
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        // 初始化table數組
        table = new Entry[capacity];
        initHashSeedAsNeeded(capacity);
    }

  從上面的代碼中能夠知道,HashMap中table數組的長度必定是2的n次方.實際上,這是一個很優雅的設計,在後面咱們還會提到。若是key不爲null,系統會調用hash()算法算出key的hash值,並據此來計算key的的存放位置.多線程

final int hash(Object k) {
        int h = hashSeed;
        // 若是key爲字符串,調用stringHash32()處理
        // 由於字符串的hashCode碼同樣的可能性大,形成hash衝突的可能性也大
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
        // 根據key的hashCode值算hash值
        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

  獲得key的hash值後,從④處的代碼知道,此時系統會根據hash值和table的長度來計算key在table數組中的存放位置.ide

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

  這個方法的設計很是巧妙,經過h&(table.length-1)來獲得該key的保存位置,而上面說到了HashMap底層數組長度老是2的n次方.當length老是2的n次方時,h&(length-1)能保證計算獲得的值老是位於table數組的索引以內.假設h=5,length=16,h&(length-1)=5;h=6,length=16,h&(length-1)=6…函數

  接下來,若是key已經存在,則替換其value值.若是不存在則調用addEntry()處理.源碼分析

void addEntry(int hash, K key, V value, int bucketIndex) {
        // 檢查HashMap容量是否達到極限(threshold)值
        if ((size >= threshold) && (null != table[bucketIndex])) {
            // 擴充table數組的容量爲以前的1倍
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        // 調用createEntry()添加key-value對到HahsMap
        createEntry(hash, key, value, bucketIndex);
    }
    void createEntry(int hash, K key, V value, int bucketIndex) {
        // 獲取table[bucketIndex]的Entry
        Entry<K,V> e = table[bucketIndex];
        // 根據key-value建立新的Entry對象,並把新建立的Entry存放到table[bucketIndex]處
        // 新Entry對象保存e對象(以前table[bucketIndex]的Entry對象)的引用,從而造成Entry鏈
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

  系統老是將新添加的Entry對象放入table[bucketIndex]—若是bucketIndex處已經有一個Entry對象,那新添加的Entry對象指向原有的Entry對象(Entry持有一個指向原Entry對象的引用,產生一個Entry鏈),若是bucketIndex處沒有Entry對象,即上面代碼中e爲null,也就是新添加的Entry對象持有一個null引用,並無產生Entry鏈.性能

  從上面整個put方法的分析來看,咱們能夠知道HashMap存儲元素的基本流程:首先根據算出key的hash值,根據hash值和table的長度計算該key的存放位置.若是key相同,則新值替換舊值.若是key不一樣,則在table[i]桶位造成Entry鏈,並且新添加的Entry位於Entry鏈的頭部(table[i]).

  上面的代碼有點多,附上put(K key,V value)方法的流程圖:

下面是HashMap的存儲示意圖:

  

2、HashMap的讀取實現

  當HashMap的每一個buckete裏存儲的Entry只是單個Entry—也就是沒有經過指針產生Entry鏈(沒有產生hash衝突)時,此時HashMap具備最好的性能(底層結構僅僅是數組,沒有產生鏈表):當程序經過key取出對應的value時,系統先計算出hash(key)值找到key在table數組的存放位置,而後取出該桶位的Entry鏈,遍歷找到key對應的value.如下是get(K key)方法的源代碼:

public V get(Object key) {
        // 若是key爲null,調用getForNullKey()處理
        if (key == null)
            return getForNullKey();
        // 獲取key所對應的Entry
        Entry<K,V> entry = getEntry(key);

        return null == entry ? null : entry.getValue();
    }
final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        // 計算hash(key)值
        int hash = (key == null) ? 0 : hash(key);
        // 遍歷Entry鏈,找到key所對應的Entry
        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 != null && key.equals(k))))
                return e;
        }
        return null;
    }

  上面的代碼很簡單,若是HashMap的每一個bucket裏只有一個Entry時,HashMap能夠根據hash(key)值快速取出table[bucket]的Entry.在發生」Hash衝突」的狀況下,table[bucket]存放的不是一個Entry,而是一個Entry鏈,系統只能按順序遍歷Entry鏈,直到找到key相等的Entry,若是要搜索的Entry位於Entry鏈的最末端(該Entry最先放入bucket),那麼系統必須循環到最後才能找到該Entry.

  概括起來簡單地說,HashMap在底層將key-value當成一個總體進行處理,這個總體就是一個Entry對象.HashMap底層採用一個Entry[]數組來保存全部的key-value對,當存儲一個Entry對象時,會根據Hash算法來決定其存儲位置;當須要取出一個Entry時,也會根據Hash算法找到其存儲位置,再取出該Entry.因而可知:HashMap快速存取的基本原理是:不一樣的東西放在不一樣的位置,須要時才能快速找到它.

3、Hash算法的性能選項 

HashMap中定義瞭如下幾個成員變量:

  ① size:HashMap中存放的Entry數量

  ② loadFactor:HashMap的負載因子

  ③  threshold:HashMap的極限容量,當HashMap的容量達到該值時,HashMap會自動擴容(threshold=loadFactory*table.length)

  HashMap默認的構造函數會建立一個初始容量爲16,負載因子爲0.75的HashMap對象.固然,咱們也能夠經過其餘構造函數指定HashMap的初始容量和負載因子.從上面的源碼分析中,咱們知道建立HashMap時的實際容量一般比initialCapacity大一些,除非咱們指定的initialCapacity參數值正好是2的n次方.固然,知道這個之後,應該在建立HashMap時將initialCapacity參數值指定爲2的n次方,這樣能夠減小系統的計算開銷.

  當建立HashMap時,有一個默認的負載因子(load factor),其默認值爲0.75,這是時間和空間成本上的一種折衷:增大負載因子能夠減小Hash表(Entry數組)所佔用的內存空間,但會增長查詢數據的時間開銷,而查詢時最頻繁的操做(HashMap的get()與put()方法都要用到查詢);減小負載因子會提升數據查詢的性能,但會增長Hash表所佔用的內存空間.

若是可以預估HashMap會保存Entry的數量,能夠再建立HashMap時指定初始容量,若是HashMap的size一直不會超過threshold(capacity*loadFactory),就無需調用resize()從新分配table數組,resize()是很耗性能的,由於要對全部的Entry從新分配位置.固然,開始就將初始化容量設置過高可能會浪費空間(系統須要建立一個長度爲capacity的Entry數組),所以建立HashMap時初始化容量也須要當心設置.

4、細數HashMap中的優雅的設計

  1. 底層數組的長度老是爲2的n次方
  2. indexFor(hash,table.length)保證每一個Entry的存儲位置都在table數組的長度範圍內
  3. 新添加的Entry老是存放在table[bucket],相同hash(key)的Entry造成Entry

  目前就發現這麼多,之後發現了再繼續補上.

  都說好的設計是成功的一半,HashMap的設計者展現了一種設計美感.

5、HashMap使用注意問題

  以本人目前的經驗來看,HashMap使用過程當中應注意兩大類問題,其一,線程安全問題,由於HashMap是非同步的,在多線程狀況下請使用ConcurrentHashMap。其二,內存泄露問題.咱們這裏只討論第二種問題.由上面的分析能夠知道,存放到HashMap的對象,強烈建議覆寫equals()和hashCode().但hashCode值的改變可能會形成內存泄露問題.看代碼:

public class HashCodeDemo {
    public static void main(String[] args) {
        User user = new User("zhangsan",22);
        Map<User,Object> map = new HashMap<User,Object>();
        map.put(user, "user is exists");
        // user is exists
        System.out.println(map.get(user));
        // 改變age值,將會改變hashCode值
        user.setAge(23);
        // null,由於user.hashCode值變化了,此時,咱們可能永遠也沒法取出該Entry對象,但HashMap持有該Entry對象的引用,這就形成了內存泄露
        System.out.println(map.get(user));
    }
}

class User{
    private String name;
    private Integer age;
    
    public User() {
    }
    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    @Override
    public int hashCode() {
        return name.hashCode()*age.hashCode();
    }
    @Override
    public boolean equals(Object obj) {
        if(obj instanceof User){
            User user = (User)obj;
            return this.name.equals(user.name)&&this.age==user.getAge()?true:false;
        }
        return false;
    }
}

 

6、自定義HashMap實現

  這裏只作簡單模擬,加深對HashMap的理解.  

  第一步,建立MyEntry類,用於封裝key-value屬性.

class MyEntry<K, V> {
    private final K key;
    private V value;
    private MyEntry<K, V> next;
    private final int hash;

    /** 構造函數 **/
    public MyEntry(K key, V value, MyEntry<K, V> next, int hash) {
        this.key = key;
        this.value = value;
        this.next = next;
        this.hash = hash;
    }

    /** 返回Entry.key **/
    public K getKey() {
        return this.key;
    }

    /** 返回Entry.value **/
    public V getValue() {
        return this.value;
    }

    /** 替換Entry.value **/
    public V setValue(V val) {
        V oldVal = value;
        this.value = val;
        return oldVal;
    }
    public MyEntry next(){
        return next;
    }
    public int hash(){
        return hash;
    }
    @Override
    public String toString() {
        return this.key + "=" + this.value;
    }

    public void setNext(MyEntry myEntry) {
        this.next = myEntry;
        
    }
}

第二步,實現MyHashMap,底層採用數組+鏈表結構.到這裏,咱們會發現,其實實現HashMap關鍵點有如下幾個:

  ① HashMap容量的管理和性能參數的設置

      ② hash()算法的實現,理想的hash算法是不會產生"hash衝突的"(HashMap底層僅僅是數組),在這種狀況下,HashMap能達到最好的存取性能.

  HashMap的設計者很好的解決了這兩個問題,關於這兩個問題,能夠參考源碼.

以上就是我對HashMap源碼的學習總結,有不正確或不許確的地方,請你們指出來!很是歡迎你們一塊兒交流學習!

以上內容參考:http://www.ibm.com/developerworks/cn/java/j-lo-hash/?ca=drs-tp4608

相關文章
相關標籤/搜索