一、介紹:
HashMap是java集合框架中常用的數據結構,其本質是一個Entry結構的數組和鏈表組成,即主體是長度為2的冪的數組,里面的元素為鏈表結構。接下來,我們來分析他的源碼組成。
二、源碼分析:

在閱讀源碼之前,我們先看看,再集合框架中,HashMap的繼承關系。HashMap根據 key 的 hashCode 值l來定位存儲數據,大多數情況下可以直接定位到它的值,因而具有很快的訪問速度(數組的查找操作是線性的)。HashMap 非線程安全,舉例子來說,當在查找hashcode時,若對某一線程返回true,如果此時恰好有一刪除操作,會造成死鎖。如果需要滿足線程安全,可以用 Collections的synchronizedMap 方法使 HashMap 具有線程安全的能力,或者使用ConcurrentHashMap 。

HashMap是數組+鏈表+紅黑樹(JDK1.8增加了紅黑樹部分)實現的,為了方便分析這里依然采用JDK1.6。實現HashMap,首先定義一個Map類的接口,再集合框架中,HashMap引用Map接口。
1 interface DIYMap<K, V>{ 2 public V put(K k,V v) ; 3 public V get(K k,V v) ; 4 5 6 public interface Entry<K,V>{ 7 Entry<?, ?> next = null; 8 public K getKey() ; 9 public V getValue() ; 10 } 11 }
接下來正式自定義HashMap實現類:
1 public class DIYHashMap<K,V> implements DIYMap<K,V> { 2 public DIYHashMap(int defalutLength, double defalutAddSizeFactor){ 3 if(defalutLength < 0){ 4 throw new IllegalAccessError("數組長度為負數") ; 5 } 6 7 if(defalutAddSizeFactor<=0 || Double.isNaN(defalutAddSizeFactor)){ 8 throw new IllegalAccessError("擴容長度不能為非正數字"); 9 } 10 11 this.defalutAddSizeFactor = defalutAddSizeFactor ; 12 this.defalutLength = tableSizeFor(defalutLength) ; 13 14 } 15 16 }
這里是自定義HashMap的構造函數,在jdk源碼中,總共有四個構造函數方便調用。里面的參數代表含義如下:
//定義初始化默認數組長度 ; private int defalutLength = 16 ; //定義默認負載因子 ; private double defalutAddSizeFactor = 0.75 ; //使用數組位置的總數 private int useSize ; //定義骨架Entry數組 ; private Entry<K,V>[] table ;
tableSize()函數是為了保證數組長度為2的冪(即定義new DIYHashMap(5),得到的數組長度是8),為什么長度一定要是2的次冪?下文會總結一下,主要是為了hash散列的時候保持分布均勻,提高空間的利用效率。
1 public int tableSizeFor(int n ){ 2 int num = n-1 ; 3 num |= num>>>1; 4 num |= num>>>2; 5 num |= num>>>4; 6 num |= num>>>8; 7 num |= num>>>16; 8 9 return (num<0) ? 1:((num>= 1<<30)? 1<<30 : num+1) ; 10 }
在實現Map接口的同時,要實現內部接口Entry,即定義數組的存儲類型。
1 private class Entry<K,V> implements DIYMap.Entry<K,V>{ 2 Entry<K,V> next = null; 3 K k; 4 V v; 5 public Entry(K k,V v,Entry<K,V> next){ 6 this.k = k; 7 this.v = v; 8 this.next = next ; 9 } 10 @Override 11 public K getKey() { 12 // TODO Auto-generated method stub 13 return k; 14 } 15 16 @Override 17 public V getValue() { 18 // TODO Auto-generated method stub 19 return v; 20 } 21 22 23 }
以上就是HashMap的初始化,在jdk源碼中,依然是按照這樣的流程進行初始化構造。在一開始數組是沒有分配長度的,在put的時候才會初始化數組長度。
1 @Override 2 public V put(K k, V v) { 3 if(table.length==0 || useSize > defalutLength * defalutAddSizeFactor){ 4 up2Size() ; 5 } 6 7 int index=getIndex(k,table.length) ; 8 Entry<K,V> entry =table[index] ; 9 Entry<K,V> newEntry =new Entry<K, V>(k, v, null) ; 10 11 if(entry == null){ 12 table[index] = newEntry ; 13 useSize++ ; 14 }else{ 15 Entry<K,V> tmp; 16 while((tmp=table[index])!=null){ 17 tmp=tmp.next ; 18 } 19 tmp.next = newEntry ; 20 } 21 return newEntry.getValue() ; 22 }
首先,在put一個元素之前,進行檢查,看當前已經使用的容量useSize 是否超過了當前的負載因子,或者是不是沒有進行數組分配。如果是,會先進行擴容方法,將數組進行初始化,或者進行2倍擴展。up2Size()邏輯下面會解釋。接下來會求出需要put的元素被放置的索引getIndex(),這里面就是主要的hash散列算法。如果求出來的索引所在的table[index]==null,就可以將put的(k,v)賦值給它,useSize++;如果不為空,證明發生了hash沖突,需要將新值放在table[index].next位置。
1 //擴展數組長度 ; 2 public void up2Size(){ 3 Entry<K,V>[] newTable = new Entry[defalutLength*2] ; 4 5 ArrayList<Entry<K,V>> entryList = new ArrayList<Entry<K,V>>() ; 6 for(int i=0;i<table.length;i++){ 7 if(table[i] == null){ 8 continue ; 9 } 10 //查找是否形成鏈表 11 findEntryByNext(table[i], entryList) ; 12 } 13 if(entryList.size() >0){ 14 useSize =0; 15 defalutLength = defalutLength*2 ; 16 table=newTable ; 17 for(Entry<K,V> entry : entryList){ 18 if(entry.next != null){ 19 entry.next = null ; 20 } 21 put(entry.getKey(), entry.getValue()) ; 22 } 23 }else{ 24 table = new Entry[defalutLength] ; 25 } 26 27 }
在up2Size()函數中,重點在於已經形成鏈表的數據如何進行重新划分。這里采用ArrayList將舊的所有的元素導出來,重新進行hash計算新的index進行導入到新的數組中。其中重點就是找到已經形成鏈表的數據。
1 public void findEntryByNext(Entry<K, V> entry, ArrayList<Entry<K, V>> entryList){ 2 if(entry!= null && entry.next != null){ 3 entryList.add(entry) ; 4 findEntryByNext(entry.next, entryList) ; 5 }else 6 entryList.add(entry) ; 7 }
寫到這里,一個put函數其實已經差不多了,這里面的重點在於hash算法,如何保證分布的均勻性。
1 private int getIndex(K k,int length ){ 2 int m=length-1 ; 3 int hashCode = k.hashCode() ; 4 hashCode = hashCode^((hashCode>>>7)^(hashCode>>>12)) ; 5 hashCode = hashCode^(hashCode>>>7)^(hashCode>>>4) ; 6 7 int index = hashCode & m ; 8 9 return index >=0 ?index :-index ; 10 }
(分析待續。。。)
get方法比put要簡單一些,本質上也是計算key的hashcode,並且得到index是否有值。希望下來自己再寫一下。
