【Java基礎復習】集合框架:HashMap手寫源碼詳解


一、介紹:

  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是否有值。希望下來自己再寫一下。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM