HashMap原理和代碼淺析

hashCode介紹

分析HashMap以前先介紹下什麼Hashcode(散列碼)。它是一個int,每一個對象都會有一個hashcode,它在內存的存放位置是放在對象的頭部(對象頭部存放的信息有hashcode,指向Class的引用,和一些有關垃圾回收信息)。須要注意的是,若是在你的類中覆蓋了Object的equals(Object)方法,那麼你必須覆蓋hashCode方法,否則,當你使用HashMap,HashSet,HashTable時會出現問題。這個問題在《effective Java中文版》這本書裏面有詳細的介紹。下面只是簡要摘除來3個主要的緣由 (摘自Object規範): 
(1)在程序的執行期間,只要對象的equals方法的比較操做所用到的信息沒有被修改,那麼對這同一對象調用屢次,hashCode方法必須始終如一的返回同一個值,在同一個應用的屢次執行過程當中,每次執行所返回的值能夠不一樣。 
(2)若是兩個對象,根據equas方法比較是想法的,那麼掉哦你給這兩個對象中任意一個對象的hashCode方法都必須產生相同的整數結果。 
(3)若是兩個對象根據equals方法比較是不相等的,那麼調用這兩個對象中任意一個對象的hashCode方法,則不必定要產生不一樣的整數結果,可是程序員應該知道,給不一樣的對象產生大相徑庭的整數結果,有可能提升散列表的性能。 
下面以String的hashCode舉個列子: 
String類重寫了Object類中的equals和hashCode方法,緣由很簡單,Object中的equals方法是指比較兩個對象是否是指向同一個引用對象,而String類指須要比較內容相不相等就能夠了。因此String覆蓋了equals方法,同時覆蓋了hashCode方法(具體爲何同時覆蓋二者參考以上3點以及http://blog.csdn.net/michaellufhl/article/details/5833188)。 
String中的hashcode算法很簡單以下:html

@Override public int hashCode() {
        int hash = hashCode;
        if (hash == 0) {
            if (count == 0) {
                return 0;
            }
            for (int i = 0; i < count; ++i) {
                hash = 31 * hash + charAt(i);
            }
            hashCode = hash;
        }
        return hash;
    }

 

好比一個字符串「abc」(a的ascii碼是97),它的hashcode算法是: 
h = 31 * 0 + 97 ==> h = 97; 
h = 31 * 97 + 98 ==> h = 3105; 
h = 31 * 3105 + 99 ==> h = 96354; 
因此「abc」的hashCode就是96354 。java

以上內容參考文檔:程序員

http://www.iteye.com/topic/838030算法

HashMap的存儲結構

HashMap是以鏈法表的形式存儲的,與其對應的是開放地址發。兩種方法的比較:鏈表法和開放地址法。鏈表法就是將相同hash值的對象組織成一個鏈表放在hash值對應的槽位;開放地址法是經過一個探測算法,當某個槽位已經被佔據的狀況下繼續查找下一個可使用的槽位。java.util.HashMap採用的鏈表法的方式,鏈表是單向鏈表,所以在刪除過程當中要本身維持prev節點。 
HashMap的存儲結構圖(來自網絡): 
這裏寫圖片描述 
HashMap的功能是經過「鍵(key)」可以快速的找到「值」。下面咱們分析下HashMap存數據的基本流程: 
一、 當調用put(key,value)時,首先獲取key的hashcode。 
二、 再把hash經過一下運算獲得一個int h. 
hash ^= (hash >>> 20) ^ (hash >>> 12); 
int h = hash ^ (hash >>> 7) ^ (hash >>> 4); 
爲何要通過這樣的運算呢?這就是HashMap的高明之處。先看個例子,一個十進制數32768(二進制1000 0000 0000 0000),通過上述公式運算以後的結果是35080(二進制1000 1001 0000 1000)。看出來了嗎?或許這樣還看不出什麼,再舉個數字61440(二進制1111 0000 0000 0000),運算結果是65263(二進制1111 1110 1110 1111),如今應該很明顯了,它的目的是讓「1」變的均勻一點,散列的本意就是要儘可能均勻分佈。那這樣有什麼意義呢?看第3步。 
三、 獲得h以後,把h與HashMap的承載量(HashMap的默認承載量length是16,能夠自動變長。在構造HashMap的時候也能夠指定一個長度。這個承載量就是上圖所描述的數組的長度。)進行邏輯與運算,即 h & (length-1),這樣獲得的結果就是一個比length小的正數,咱們把這個值叫作index。其實這個index就是索引將要插入的值在數組中的位置。第2步那個算法的意義就是但願可以得出均勻的index,這是HashTable的改進,HashTable中的算法只是把key的hashcode與length相除取餘,即hash % length,這樣有可能會形成index分佈不均勻。還有一點須要說明,HashMap的鍵能夠爲null,它的值是放在數組的第一個位置。 
四、 咱們用table[index]表示已經找到的元素須要存儲的位置。先判斷該位置上有沒有元素(這個元素是HashMap內部定義的一個類Entity,基本結構它包含三個類,key,value和指向下一個Entity的next),沒有的話就建立一個Entity數組

static class Entry implements Map.Entry
{
        final K key;
        V value;
        Entry next;
        final int hash;
        ...//More code goes here
}

 

每當往hashmap裏面存放key-value對的時候,都會爲它們實例化一個Entry對象,這個Entry對象就會存儲在前面提到的Entry數組table中。如今你必定很想知道,上面建立的Entry對象將會存放在具體哪一個位置(在table中的精確位置)。答案就是,根據key的hashcode()方法計算出來的hash值(來決定)。hash值用來計算key在Entry數組的索引。網絡

存儲機制分析

原文地址:數據結構

http://www.zuidaima.com/share/1850411710188544.htmoracle

HashMap的構造方法: 
無參構造方法:會使用默認的初始容量和加載因子初始化map,默認初始化大小是16,加載因子0.75f。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 rehash 操做(即重建內部數據結構),從而哈希表將具備大約兩倍的桶數。app

/**
 * The default initial capacity - MUST be a power of two.
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
 * The load factor used when none specified in constructor.
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
       this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}

 

自定義初始化大小ide

/**
   * Constructs an empty <tt>HashMap</tt> with the specified initial
   * capacity and the default load factor (0.75).
   *
   * @param  initialCapacity the initial capacity.
   * @throws IllegalArgumentException if the initial capacity is negative.
   */
  public HashMap(int initialCapacity) {
      this(initialCapacity, DEFAULT_LOAD_FACTOR);
  }

 

自定義初始化大小和加載因子

/**
     * Constructs an empty <tt>HashMap</tt> with the specified initial
     * capacity and load factor.
     *
     * @param  initialCapacity the initial capacity
     * @param  loadFactor      the load factor
     * @throws IllegalArgumentException if the initial capacity is negative
     *         or the load factor is nonpositive
     */
    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;
        threshold = initialCapacity;
        init();
    }

 

總結:當 建立 HashMap 時,有一個默認的負載因子(load factor),其默認值爲 0.75,這是時間和空間成本上一種折衷:增大負載因子能夠減小 Hash 表(就是那個 Entry 數組)所佔用的內存空間,但會增長查詢數據的時間開銷,而查詢是最頻繁的的操做(HashMap 的 get() 與 put() 方法都要用到查詢);減少負載因子會提升數據查詢的性能,但會增長 Hash 表所佔用的內存空間。咱們能夠在建立 HashMap 時根據實際須要適當地調整 load factor 的值;若是程序比較關心空間開銷、內存比較緊張,能夠適當地增長負載因子;若是程序比較關心時間開銷,內存比較寬裕則能夠適當的減小負載因子。一般狀況 下,無需改變負載因子的值。 
HashMap最經常使用的put方法,代碼以下:

/**
    * Associates the specified value with the specified key in this map.
    * If the map previously contained a mapping for the key, the old
    * value is replaced.
    *
    * @param key key with which the specified value is to be associated
    * @param value value to be associated with the specified key
    * @return the previous value associated with <tt>key</tt>, or
    *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
    *         (A <tt>null</tt> return can also indicate that the map
    *         previously associated <tt>null</tt> with <tt>key</tt>.)
    */
   public V put(K key, V value) {
       //若是數組爲空,初始化
       if (table == EMPTY_TABLE) {
           inflateTable(threshold);
       }
       //若是key爲空,則調用putForNullKey進行處理
       if (key == null)
           return putForNullKey(value);
       int hash = hash(key);//計算key的hashcode值
       int i = indexFor(hash, table.length);//計算key在hash表中的索引,此處的table是一個Entry<k,v>數組
       //遍歷數組,比較Entry是否一致(hash值相等,即在hash表中的同一位置),而且key值相等,則直接用新的value替換舊的value並返回value,key值不用替換。若是不知足條件,則將key和value添加到i索引處
       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;
           }
       }

       modCount++;
      //將key和value添加到i索引處
       addEntry(hash, key, value, i);
       return null;
   }

 

上面的put方法中用到了一個重要的內部類HashMap$Entry,每一個 Entry 其實就是一個 key-value 對。從上面程序中能夠看出:當系統決定存儲 HashMap 中的 key-value 對時,徹底沒有考慮 Entry 中的 value,僅僅只是根據 key 來計算並決定每一個 Entry 的存儲位置。當決定了 key 的存儲位置以後,value 隨之保存在那裏便可,Entry源碼以下:

static class Entry<K,V> implements Map.Entry<K,V> {
       final K key;//key值
       V value;//value值
       Entry<K,V> next;//Entry鏈指向
       int hash;//key的hash值

       /**
        * Creates new entry.
        */
       Entry(int h, K k, V v, Entry<K,V> n) {
           value = v;
           next = n;
           key = k;
           hash = h;
       }

       public final K getKey() {
           return key;
       }

       public final V getValue() {
           return value;
       }

       public final V setValue(V newValue) {
           V oldValue = value;
           value = newValue;
           return oldValue;
       }

       public final boolean equals(Object o) {
           if (!(o instanceof Map.Entry))
               return false;
           Map.Entry e = (Map.Entry)o;
           Object k1 = getKey();
           Object k2 = e.getKey();
           if (k1 == k2 || (k1 != null && k1.equals(k2))) {
               Object v1 = getValue();
               Object v2 = e.getValue();
               if (v1 == v2 || (v1 != null && v1.equals(v2)))
                   return true;
           }
           return false;
       }

       public final int hashCode() {
           return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
       }

       public final String toString() {
           return getKey() + "=" + getValue();
       }

       /**
        * This method is invoked whenever the value in an entry is
        * overwritten by an invocation of put(k,v) for a key k that's already
        * in the HashMap.
        */
       void recordAccess(HashMap<K,V> m) {
       }

       /**
        * This method is invoked whenever the entry is
        * removed from the table.
        */
       void recordRemoval(HashMap<K,V> m) {
       }
   }

 

put方法中調用了一個計算Hash碼的方法hash()來返回key的哈希碼,這個方法是一個純粹的數學計算,其方法以下:

final int hash(Object k) {
      int h = hashSeed;
      if (0 != h && k instanceof String) {
          return sun.misc.Hashing.stringHash32((String) k);
      }

      h ^= k.hashCode();

      // This function ensures that hashCodes that differ only by
      // constant multiples at each bit position have a bounded
      // number of collisions (approximately 8 at default load factor).
      h ^= (h >>> 20) ^ (h >>> 12);
      return h ^ (h >>> 7) ^ (h >>> 4);
  }

 

對於任意給定的對象,只要它的 hashCode() 返回值相同,那麼程序調用 hash(int h) 方法所計算獲得的 Hash 碼值老是相同的。接下來程序會調用 indexFor(int h, int length) 方法來計算該對象應該保存在 table 數組的哪一個索引處。indexFor(int h, int length) 方法的代碼以下:

//h爲key的hash值,length爲數組的長度
static int indexFor(int h, int length) 
{ 
    return h & (length-1); 
}

 

這個方法很是巧妙,它老是經過 h &(table.length -1) 來獲得該對象的保存位置,而HashMap底層數組的長度老是2的n次方,這一點可參看前面關於HashMap構造器的介紹。

當length老是2的倍數時,h&(length-1)將是一個很是巧妙的設計:假設 h=5,length=16, 那麼h&(length - 1) 將獲得5;若是h=6,length=16, 那麼h&(length - 1)將獲得6 ;若是h=15,length=16, 那麼h&(length - 1)將獲得15;可是當h=16時 ,length=16時,那麼h&(length - 1)將獲得0了;當 h=17 時 , length=16 時,那麼h&(length - 1) 將獲得1了……這樣保證計算獲得的索引值老是位於 table 數組的索引以內。

從put 方法的源代碼能夠看出,當程序試圖將一個 key-value 對放入 HashMap 中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:若是兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。存儲位置相同會分爲兩種狀況:

(1).若是這兩個 Entry 的 key 經過 equals 比較返回 true,新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但 key 不會覆蓋。

(2).若是這兩個 Entry 的 key 經過 equals 比較返回 false,新添加的 Entry 將與集合中原有 Entry 造成 Entry 鏈,並且新添加的 Entry 位於 Entry 鏈的頭部——具體說明繼續看 addEntry() 方法的說明。

存儲位置不一樣,則將key和value直接添加到i索引處。

addEntyr方法,源碼以下:

void addEntry(int hash, K key, V value, int bucketIndex) {
    //若是容量大於閾值,而且索引bucketIndex處的元素不爲空       
    if ((size >= threshold) && (null != table[bucketIndex])) {
         resize(2 * table.length);//擴容爲原來數組長度的兩倍
         hash = (null != key) ? hash(key) : 0;//從新計算key的hash值
         bucketIndex = indexFor(hash, table.length);//從新計算元素在新table中的索引
     }
     //建立新的entry對象並放到table的bucketIndex索引處,並讓新的entry指向原來的entry
     createEntry(hash, key, value, bucketIndex);
 }

 void createEntry(int hash, K key, V value, int bucketIndex) {
     Entry<K,V> e = table[bucketIndex];
     table[bucketIndex] = new Entry<>(hash, key, value, e);
     size++;
 }

 

上面createEntry方法包含了一個很是優雅的設計:老是將新添加的 Entry 對象放入 table 數組的 bucketIndex 索引處——若是 bucketIndex 索引處已經有了一個 Entry 對象,那新添加的 Entry 對象指向原有的 Entry 對象(產生一個 Entry 鏈),若是 bucketIndex 索引處沒有 Entry 對象,上面程序 e 變量是 null,也就是新放入的 Entry 對象指向 null,也就是Entry內部類中的next屬性爲null,也就是沒有產生 Entry 鏈。,能夠對比Entry類看。

解釋幾個名詞:

桶:對 於 HashMap 及其子類而言,它們採用 Hash 算法來決定集合中元素的存儲位置。當開始初始化 HashMap 時,會建立一個長度爲 capacity 的 Entry 數組,這個數組裏能夠存儲元素的位置被稱爲「桶(bucket)」,每一個 bucket 都有其指定索引,系統能夠根據其索引快速訪問該 bucket 裏存儲的元素。

Entry鏈:不管什麼時候,HashMap 的每一個「桶」只存儲一個元素(也就是一個 Entry),因爲 Entry 對象能夠包含一個引用變量(就是 Entry 構造器的的最後一個參數next)用於指向下一個 Entry,所以可能出現的狀況是:HashMap 的 bucket 中只有一個 Entry,但這個 Entry 指向另外一個 Entry ——這就造成了一個 Entry 鏈。下圖爲我簡單的畫了一個HashMap的存儲結構: 
這裏寫圖片描述

經過Java HashMap的存取方式來學習Hash存儲機制 
HashMap最經常使用的get方法,源碼以下:

public V get(Object key) {
      //若是key爲null,則調用getForNullKey得到value
      if (key == null)
          return getForNullKey();
      //不然調用getEntry方法
      Entry<K,V> entry = getEntry(key);

      return null == entry ? null : entry.getValue();
 }

 final Entry<K,V> getEntry(Object key) {
        if (size == 0) {
            return null;
        }
        //計算key的hash值
        int hash = (key == null) ? 0 : hash(key);
        //直接經過key的hash值獲取該Entry在數組中的下標,從而獲取該Entry對象並遍歷entry鏈,直到找到相等的key,而後取出該key對應的value。
        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 能夠根據索引、快速地取出該 bucket 裏的 Entry;在發生「Hash 衝突」的狀況下,單個 bucket 裏存儲的不是一個 Entry,而是一個 Entry 鏈,只能按順序遍歷每一個 Entry,直到找到想搜索的 Entry 爲止——若是剛好要搜索的 Entry 位於該 Entry 鏈的最末端(該 Entry 是最先放入該 bucket 中),那必須循環到最後才能找到該元素。因此,當 HashMap 的每一個 bucket 裏存儲的 Entry 只是單個 Entry ,也就是沒有經過指針產生 Entry 鏈時,此時的 HashMap 具備最好的性能:當程序經過 key 取出對應 value 時,只要先計算出該 key 的 hashCode() 返回值,在根據該 hashCode 返回值找出該 key 在 table 數組中的索引,而後取出該索引處的 Entry,最後返回該 key 對應的 value 便可。

算是本身學習的Map的網上資料的一個彙總(當是備忘錄了吧)。 
附上本身感受比較好的文檔:

http://carmen-hongpeng.iteye.com/blog/1706415#

http://www.oracle.com/technetwork/cn/articles/maps1-100947-zhs.html

http://zhangshixi.iteye.com/blog/672697

http://www.iteye.com/topic/838030

相關文章
相關標籤/搜索