1. 程式人生 > >1.Java集合-HashMap實現原理及源碼分析

1.Java集合-HashMap實現原理及源碼分析

int -1 詳細 鏈接 理解 dac hash函數 順序存儲結構 對象儲存

  哈希表(Hash Table)也叫散列表,是一種非常重要的數據結構,應用場景及其豐富,許多緩存技術(比如memcached)的核心其實就是在內存中維護一張大的哈希表,而HashMap的實現原理也常常出現在各類的面試題中,這裏對java集合框架中的對應實現HashMap的實現原理進行講解,然後對JDK7的HashMap的源碼進行分析

  哈希算法,是一類算法;

  哈希表(Hash Table)是一種數據結構;

  哈希函數:是支撐哈希表的一類函數;

  HashMap 是 Java中用哈希數據結構實現的Map

一、什麽是哈希表

  在討論哈希表之前,我們先大概了解下其他數據結構在新增、查找等基礎操作執行性能

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

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

  二叉樹:對一棵相對平衡的二叉樹,對其進行插入,查找,刪除等操作,平均復雜度均為O(logn)(這個時間復雜度常見於二分)

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

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

  比如我們要新增或查找某個元素,我們通過把當前元素的關鍵字通過某個函數映射到數組中的某個位置,通過數組下標一次定位就可完成操作

  存儲位置 = f(關鍵字)


  其中這個函數f一般稱為 哈希函數,這個函數設計的好壞會直接影響到哈希表的優劣。舉個例子,比如我們要在哈希表中執行插入操作:

  技術分享

  哈希沖突

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

  

二、HashMap實現原理

  HashMap的主幹是一個Entry數組。Entry是HashMap的基本組成單元,每一個Entry包含一個key-value鍵值對。

1 //HashMap的主幹數組,可以看到就是一個Entry數組,初始值為空數組{},主幹數組的長度一定是2的次冪,至於為什麽這麽做,後面會有詳細分析。
2 transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

  Entry是HashMap中的一個靜態內部類。代碼如下:

 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         int hash;//對key的hashcode值進行hash運算後得到的值,存儲在Entry,避免重復計算
 6 
 7         /**
 8          * Creates new entry.
 9          */
10         Entry(int h, K k, V v, Entry<K,V> n) {
11             value = v;
12             next = n;
13             key = k;
14             hash = h;
15 }

所以,HashMap的整體結構如下:

技術分享

  簡單來說,HashMap由數組 + 鏈表組成的,數組是HashMap的主體,鏈表則是主要為了解決哈希沖突而存在的,如果定位到的數組位置不含鏈表(當前entry的next指向null),那麽對於查找,添加等操作很快,僅需一次尋址即可;如果定位到的數組包含鏈表,對於添加操作,其時間復雜度依然為O(1),因為最新的Entry會插入到鏈表頭部,僅需要簡單改變引用鏈即可,而對於查找操作來講,此時就需要遍歷鏈表,然後通過key對象的equals方法逐一對比查找。所以,性能考慮,HashMap中的鏈表出現越少,性能才會越好。

  HashMap類中的一些關鍵屬性:

1 //實際存儲的key-value鍵值對的個數
2 transient int size;
3 //閾值;當table被填充了,也就是為table分配內存空間後(初始容量默認為16),threshold一般為 capacity*loadFactory。HashMap在進行擴容時需要參考threshold,後面會詳細談到
4 int threshold;  //臨界值,當實際大小超過臨界值時,會進行擴容  threshold = 加載因子*容量
5 //負載因子,代表了table的填充度有多少,默認是0.75
6 final float loadFactor;
7 //用於快速失敗,由於HashMap非線程安全,在對HashMap進行叠代時,如果期間其他線程的參與導致HashMap的結構發生變化了(比如put,remove等操作),需要拋出異常ConcurrentModificationException
8 transient int modCount;

  註:threshold在這個版本的源碼中,起到了兩個作用,一個是代表了table的默認初始容量,一個是需要對 HashMap進行擴容的臨界值,在對table進行初始化後,就會改變threshold的值,改為加載因子*容量

  其中loadFactor 加載因子是表示Hash表中元素的填滿的程度

  若:加載因子越大,填滿的元素越多,好處是,空間利用率高了,但:沖突的機會大了,鏈表的常長度會越來越長,查找的效率降低

  反之,加載因子越小,填滿的元素越少,好處是:沖突的機會減小了,但:空間浪費多了,表中的數據過於稀疏(很多空間還沒用,就開始擴容了)

  因此,必須在“沖突的機會” 與 “空間利用率”之間尋找一個平衡與折中,本質上這就是數據結構中有名的 “時-空”矛盾的平衡和折中(一般都不用去設置它,讓它取默認值0.75就好了)

    

  HashMap有四個構造器,如果用戶在創建HashMap的時候,沒有傳入initialCapacity 和 loadFactor這兩個參數,會使用默認值,initialCapacity默認為16,loadFactor默認為0.75

 1 public HashMap(int initialCapacity, float loadFactor) {
 2      //此處對傳入的初始容量進行校驗,最大不能超過MAXIMUM_CAPACITY = 1<<30(230)
 3         if (initialCapacity < 0)
 4             throw new IllegalArgumentException("Illegal initial capacity: " +
 5                                                initialCapacity);
 6         if (initialCapacity > MAXIMUM_CAPACITY)
 7             initialCapacity = MAXIMUM_CAPACITY;
 8         if (loadFactor <= 0 || Float.isNaN(loadFactor))
 9             throw new IllegalArgumentException("Illegal load factor: " +
10                                                loadFactor);
11 
12         this.loadFactor = loadFactor;
13         threshold = initialCapacity;
14      
15         init();//init方法在HashMap中沒有實際實現,不過在其子類如 linkedHashMap中就會有對應實現
16 }

  從上面這段代碼可以看出,在常規構造器中,沒有為數組table分配內存空間(有一個入參為指定Map的構造器例外),而是在執行put操作的時候才真正構建table數組

接下來我們來看看put操作的實現

 1 public V put(K key, V value) {
 2         //如果table數組為空數組{},進行數組填充(為table分配實際內存空間),入參為threshold,此時threshold為initialCapacity 默認是1<<4(24=16)
 3         if (table == EMPTY_TABLE) {
 4             inflateTable(threshold);
 5         }
 6        //如果key為null,存儲位置為table[0]或table[0]的沖突鏈上
 7         if (key == null)
 8             return putForNullKey(value);
 9         int hash = hash(key);//對key的hashcode進一步計算,確保散列均勻
10         int i = indexFor(hash, table.length);//獲取在table中的實際位置
11         for (Entry<K,V> e = table[i]; e != null; e = e.next) {
12         //如果該對應數據已存在,執行覆蓋操作。用新value替換舊value,並返回舊value
13             Object k;
14             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
15                 V oldValue = e.value;
16                 e.value = value;
17                 e.recordAccess(this);
18                 return oldValue;
19             }
20         }
21         modCount++;//保證並發訪問時,若HashMap內部結構發生變化,快速響應失敗
22         addEntry(hash, key, value, i);//新增一個entry
23         return null;
24 }

  每一個被添加的元素都有一個 hashCode(哈希值),他們先比較哈希值,是否相同? 不相同的元素,添加進入 table.

  如果hashCode相同的話, 再去比較 equals()方法,如果不相同的話,因為hashcode的值是一樣的,所以插入在table數組中的位置是一樣的,所以會在這個位置以鏈表的形式進行存儲

  如果 equals()也相同的話,JVM就認為數據已經存在了,就不會添加數據!

先看下inflateTable這個方法:

1 private void inflateTable(int toSize) {
2         int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次冪
3         threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此處為threshold賦值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不會超過MAXIMUM_CAPACITY,除非loadFactor大於1
4         table = new Entry[capacity];
5         initHashSeedAsNeeded(capacity);
6 }

inflateTable這個方法用於為主幹數組table在內存中分配存儲空間,通roundUpToPowerOf2(toSize)可以確保capacity為大於或等於toSize的最接近toSize的二次冪,比如toSize=13,則capacity=16;to_size=16,capacity=16;to_size=17,capacity=32.

1 private static int roundUpToPowerOf2(int number) {
2         // assert number >= 0 : "number must be non-negative";
3         return number >= MAXIMUM_CAPACITY
4                 ? MAXIMUM_CAPACITY
5                 : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
6 }

roundUpToPowerOf2中的這段處理使得數組長度一定為2的次冪,Integer.highestOneBit是用來獲取最左邊的bit(其他bit位為0)所代表的數值.

 

hash函數

 1 //這是一個神奇的函數,用了很多的異或,移位等運算,對key的hashcode進一步進行計算以及二進制位的調整等來保證最終獲取的存儲位置盡量分布均勻
 2 final int hash(Object k) {
 3         int h = hashSeed;
 4         if (0 != h && k instanceof String) {
 5             return sun.misc.Hashing.stringHash32((String) k);
 6         }
 7 
 8         h ^= k.hashCode();
 9 
10         h ^= (h >>> 20) ^ (h >>> 12);
11         return h ^ (h >>> 7) ^ (h >>> 4);
12  }

以上hash函數計算出的值,通過indexFor進一步處理來獲取實際的存儲位置

1 /**
2      * 返回數組下標
3      */
4     static int indexFor(int h, int length) {
5         return h & (length-1);
6     }

  我們一般對哈希表的散列很自然地會想到用hash值對length取模(即除法散列法),Hashtable中也是這樣實現的,這種方法基本能保證元素在哈希表中散列的比較均勻,但取模會用到除法運算,效率很低,HashMap中則通過h&(length-1)的方法來代替取模,同樣實現了均勻的散列,但效率要高很多,這也是HashMap對Hashtable的一個改進。

h&(length-1)保證獲取的index一定在數組範圍內,舉個例子,默認容量16,length-1=15,h=18,轉換成二進制計算為

技術分享

最終計算出的index=2。有些版本的對於此處的計算會使用 取模運算,也能保證index一定在數組範圍內,不過位運算對計算機來說,性能更高一些(HashMap中有大量位運算)

所以最終存儲位置的確定流程是這樣的:

技術分享·

再來看看addEntry的實現:

1 void addEntry(int hash, K key, V value, int bucketIndex) {
2         if ((size >= threshold) && (null != table[bucketIndex])) {
3             resize(2 * table.length);//當size超過臨界閾值threshold,並且即將發生哈希沖突時進行擴容
4             hash = (null != key) ? hash(key) : 0;
5             bucketIndex = indexFor(hash, table.length);
6         }
7 
8         createEntry(hash, key, value, bucketIndex);
9 }

  通過以上代碼能夠得知,當發生哈希沖突並且size大於閾值的時候,需要進行數組擴容,擴容時,需要新建一個長度為之前數組2倍的新的數組,然後將當前的Entry數組中的元素全部傳輸過去,擴容後的新數組長度為之前的2倍,所以擴容相對來說是個耗資源的操作。所以如果我們已經預知HashMap中元素的個數,那麽預設元素的個數能夠有效的提高HashMap的性能。

三、為什麽HashMap的數組長度一定是2的次冪?

  因為在這個版本的HashMap中,不是用取模運算(hash值對length取模),是通過位運算h&(length-1)來使元素在哈希表中散列的比較均勻,使用位運算的前提就是要保證數組長度是2的次冪,下面我們具體說明:

  首先,length為2的整數次冪的話,h&(length-1)就相當於對length取模,這樣便保證了散列的均勻,同時也提升了效率;

  其次,length為2的整數次冪的話,就一定是偶數,這樣length-1為奇數,奇數的最後一位是1,這樣便保證了h&(length-1)的最後一位可能為0,也可能為1(這取決於h的值)。即&後的結果可能為偶數,也可能為奇數,這樣便可保證散列的均勻性

  而如果length為奇數的話,很明顯length-1就一定為偶數,它的最後一位是0,這樣h&(length-1)的最後一位肯定為0,計算後的結果只能為偶數,這樣任何hash值都只會被散列到數組的偶數下標位置上,這便浪費了近一半的空間,因此length取2的整數次冪,是為了使不同hash值發生碰撞的概率較小,這樣就能使元素在哈希表中均勻地散列

接下來我們看看get方法

1 public V get(Object key) {
2      //如果key為null,則直接去table[0]處去檢索即可。
3         if (key == null)
4             return getForNullKey();
5         Entry<K,V> entry = getEntry(key);
6         return null == entry ? null : entry.getValue();
7  }

get方法通過key值返回對應value,如果key為null,直接去table[0]處檢索。我們再看一下getEntry這個方法

 1 final Entry<K,V> getEntry(Object key) {
 2             
 3         if (size == 0) {
 4             return null;
 5         }
 6         //通過key的hashcode值計算hash值
 7         int hash = (key == null) ? 0 : hash(key);
 8         //indexFor (hash&length-1) 獲取最終數組索引,然後遍歷鏈表,通過equals方法比對找出對應記錄
 9         for (Entry<K,V> e = table[indexFor(hash, table.length)];
10              e != null;
11              e = e.next) {
12             Object k;
13             if (e.hash == hash && 
14                 ((k = e.key) == key || (key != null && key.equals(k))))
15                 return e;
16         }
17         return null;
18 }

  從上面的源碼可以看出,get方法的實現相對簡單,key(hashcode)-->hash-->indexFor-->最終索引位置,找到對應位置 table[i],再查看是否有鏈表,如果有鏈表,遍歷鏈表,通過key的equals方法比對查找對應的記錄(從鏈表中存取元素,都需要用到equals方法來判斷是否key是否相等)

  要註意的是,有人覺得上面在定位到數組位置之後然後遍歷鏈表的時候,e.hash == hash這個判斷沒必要,僅通過equals判斷就可以。其實不然,試想一下,如果傳入的key對象重寫了equals方法卻沒有重寫hashCode,而恰巧此對象定位到這個數組位置,如果僅僅用equals判斷可能是相等的,但其hashCode和當前對象不一致,這種情況,根據Object的hashCode的約定,不能返回當前對象,而應該返回null,後面的例子會做出進一步解釋。

四、重寫equals方法需同時重寫hashcode方法

很多時候都會聽到,“重寫equals時也要同時覆蓋hashcode”,我們舉個小例子來看看,如果重寫了equals而不重寫hashcode會發生什麽樣的問題

 1 /**
 2  * Created by chengxiao on 2016/11/15.
 3  */
 4 public class MyTest {
 5     private static class Person{
 6         int idCard;
 7         String name;
 8 
 9         public Person(int idCard, String name) {
10             this.idCard = idCard;
11             this.name = name;
12         }
13         @Override
14         public boolean equals(Object o) {
15             if (this == o) {
16                 return true;
17             }
18             if (o == null || getClass() != o.getClass()){
19                 return false;
20             }
21             Person person = (Person) o;
22             //兩個對象是否等值,通過idCard來確定
23             return this.idCard == person.idCard;
24         }
25 
26     }
27     public static void main(String []args){
28         HashMap<Person,String> map = new HashMap<Person, String>();
29         Person person = new Person(1234,"喬峰");
30         //put到hashmap中去
31         map.put(person,"天龍八部");
32         //get取出,從邏輯上講應該能輸出“天龍八部”
33         System.out.println("結果:"+map.get(new Person(1234,"蕭峰")));
34     }
35 }

  如果我們已經對HashMap的原理有了一定了解,這個結果就不難理解了。盡管我們在進行get和put操作的時候,使用的key從邏輯上講是等值的(通過equals比較是相等的),但由於沒有重寫hashCode方法,所以put操作時,key(hashcode1)-->hash-->indexFor-->最終索引位置 ,而通過key取出value的時候 key(hashcode1)-->hash-->indexFor-->最終索引位置,由於hashcode1不等於hashcode2,導致沒有定位到一個數組位置而返回邏輯上錯誤的值null(也有可能碰巧定位到一個數組位置,但是也會判斷其entry的hash值是否相等,上面get方法中有提到。)

  所以,在重寫equals的方法的時候,必須註意重寫hashCode方法,同時還要保證通過equals判斷相等的兩個對象,調用hashCode方法要返回同樣的整數值。而如果equals判斷不相等的兩個對象,其hashCode可以相同(只不過會發生哈希沖突,應盡量避免)。

總結:HashMap的工作原理

HashMap基於hashing原理,我們通過put()和get()方法存儲和獲取對象。

都是 key(hashcode1)-->hash-->indexFor-->最終索引位置 都是這樣一個過程

當發生哈希碰撞時(不同元素最終得到的hash值可能相同,即數組索引可能相同),HashMap即是采用了鏈地址法來解決,也就是數組 + 鏈表的方式,鏈表中的元素通過equlas方法來判斷後進行存取

最後,看一下HashMap有關的一些面試問題:

先來些簡單的問題

“你用過HashMap嗎?” “什麽是HashMap?你為什麽用到它?”

  幾乎每個人都會回答“是的”,然後回答HashMap的一些特性,譬如HashMap可以接受null鍵值和值,而Hashtable則不能;HashMap是非synchronized;HashMap很快;以及HashMap儲存的是鍵值對等等。這顯示出你已經用過HashMap,而且對它相當的熟悉。但是面試官來個急轉直下,從此刻開始問出一些刁鉆的問題,關於HashMap的更多基礎的細節。面試官可能會問出下面的問題:

“你知道HashMap的工作原理嗎?” “你知道HashMap的get()方法的工作原理嗎?”

  你也許會回答“我沒有詳查標準的Java API,你可以看看Java源代碼或者Open JDK。”“我可以用Google找到答案。”

  但一些面試者可能可以給出答案,“HashMap是基於hashing的原理,我們使用put(key, value)存儲對象到HashMap中,使用get(key)從HashMap中獲取對象。當我們給put()方法傳遞鍵和值時,我們先對鍵調用hashCode()方法,返回的hashCode用於找到bucket位置來儲存Entry對象。”這裏關鍵點在於指出,HashMap是在bucket中儲存鍵對象和值對象,作為Map.Entry。這一點有助於理解獲取對象的邏輯。如果你沒有意識到這一點,或者錯誤的認為僅僅只在bucket中存儲值的話,你將不會回答如何從HashMap中獲取對象的邏輯。這個答案相當的正確,也顯示出面試者確實知道hashing以及HashMap的工作原理。但是這僅僅是故事的開始,當面試官加入一些Java程序員每天要碰到的實際場景的時候,錯誤的答案頻現。下個問題可能是關於HashMap中的碰撞探測(collision detection)以及碰撞的解決方法:

“當兩個對象的hashcode相同會發生什麽?”

  從這裏開始,真正的困惑開始了,一些面試者會回答因為hashcode相同,所以兩個對象是相等的,HashMap將會拋出異常,或者不會存儲它們。然後面試官可能會提醒他們有equals()和hashCode()兩個方法,並告訴他們兩個對象就算hashcode相同,但是它們可能並不相等。一些面試者可能就此放棄,而另外一些還能繼續挺進,他們回答“因為hashcode相同,所以它們的bucket位置相同,‘碰撞’會發生。因為HashMap使用鏈表存儲對象,這個Entry(包含有鍵值對的Map.Entry對象)會存儲在鏈表中。”這個答案非常的合理,雖然有很多種處理碰撞的方法,這種方法是最簡單的,也正是HashMap的處理方法。但故事還沒有完結,面試官會繼續問:

“如果兩個鍵的hashcode相同,你如何獲取值對象?”

  面試者會回答:當我們調用get()方法,HashMap會使用鍵對象的hashcode找到bucket位置,然後獲取值對象。面試官提醒他如果有兩個值對象儲存在同一個bucket,他給出答案:將會遍歷鏈表直到找到值對象。面試官會問因為你並沒有值對象去比較,你是如何確定確定找到值對象的?除非面試者直到HashMap在鏈表中存儲的是鍵值對,否則他們不可能回答出這一題。

  其中一些記得這個重要知識點的面試者會說,找到bucket位置之後,會調用keys.equals()方法去找到鏈表中正確的節點,最終找到要找的值對象。完美的答案!

  許多情況下,面試者會在這個環節中出錯,因為他們混淆了hashCode()和equals()方法。因為在此之前hashCode()屢屢出現,而equals()方法僅僅在獲取值對象的時候才出現。一些優秀的開發者會指出使用不可變的、聲明作final的對象,並且采用合適的equals()和hashCode()方法的話,將會減少碰撞的發生,提高效率。不可變性使得能夠緩存不同鍵的hashcode,這將提高整個獲取對象的速度,使用String,Interger這樣的wrapper類作為鍵是非常好的選擇(wrapper類正好滿足前面提到的)。

如果你認為到這裏已經完結了,那麽聽到下面這個問題的時候,你會大吃一驚。“如果HashMap的大小超過了負載因子(load factor)定義的容量,怎麽辦?”

  除非你真正知道HashMap的工作原理,否則你將回答不出這道題。默認的負載因子大小為0.75,也就是說,當一個map填滿了75%的bucket時候,和其它集合類(如ArrayList等)一樣,將會創建原來HashMap大小的兩倍的bucket數組,來重新調整map的大小,並將原來的對象放入新的bucket數組中。這個過程叫作rehashing,因為它調用hash方法找到新的bucket位置。

如果你能夠回答這道問題,下面的問題來了:“你了解重新調整HashMap大小存在什麽問題嗎?”你可能回答不上來,這時面試官會提醒你當多線程的情況下,可能產生條件競爭(race condition)。

當重新調整HashMap大小的時候,確實存在條件競爭,因為如果兩個線程都發現HashMap需要重新調整大小了,它們會同時試著調整大小。在調整大小的過程中,存儲在鏈表中的元素的次序會反過來,因為移動到新的bucket位置的時候,HashMap並不會將元素放在鏈表的尾部,而是放在頭部,這是為了避免尾部遍歷(tail traversing)。如果條件競爭發生了,那麽就死循環了。這個時候,你可以質問面試官,為什麽這麽奇怪,要在多線程的環境下使用HashMap呢?:)

熱心的讀者貢獻了更多的關於HashMap的問題:

為什麽String, Interger這樣的wrapper類適合作為鍵?

   String, Interger這樣的wrapper類作為HashMap的鍵是再適合不過了,而且String最為常用。因為String是不可變的,也是final的,而且已經重寫了equals()和hashCode()方法了。其他的wrapper類也有這個特點。不可變性是必要的,因為為了要計算hashCode(),就要防止鍵值改變,如果鍵值在放入時和獲取時返回不同的hashcode的話,那麽就不能從HashMap中找到你想要的對象。不可變性還有其他的優點如線程安全。如果你可以僅僅通過將某個field聲明成final就能保證hashCode是不變的,那麽請這麽做吧。因為獲取對象的時候要用到equals()和hashCode()方法,那麽鍵對象正確的重寫這兩個方法是非常重要的。如果兩個不相等的對象返回不同的hashcode的話,那麽碰撞的幾率就會小些,這樣就能提高HashMap的性能。

我們可以使用自定義的對象作為鍵嗎?

  這是前一個問題的延伸。當然你可能使用任何對象作為鍵,只要它遵守了equals()和hashCode()方法的定義規則,並且當對象插入到Map中之後將不會再改變了。如果這個自定義對象時不可變的,那麽它已經滿足了作為鍵的條件,因為當它創建之後就已經不能改變了。

  我們可以使用CocurrentHashMap來代替Hashtable嗎?

這是另外一個很熱門的面試題,因為ConcurrentHashMap越來越多人用了。我們知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因為它僅僅根據同步級別對map的一部分進行上鎖。ConcurrentHashMap當然可以代替HashTable,但是HashTable提供更強的線程安全性。

讓我們再來看看這些問題設計哪些知識點:

  hashing的概念

  HashMap中解決碰撞的方法

  equals()和hashCode()的應用,以及它們在HashMap中的重要性

  不可變對象的好處

  HashMap多線程的條件競爭

  重新調整HashMap的大小

參考:http://www.cnblogs.com/chengxiao/p/6059914.html

  http://www.cnblogs.com/ITtangtang/p/3948798.html 

作者: dreamcatcher-cx

出處: <http://www.cnblogs.com/chengxiao/>

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在頁面明顯位置給出原文鏈接。

 

  

  

1.Java集合-HashMap實現原理及源碼分析