HashMap(jdk1.7)瞭解一下

Map 接口

Map 有鍵和值得概念,一個鍵映射到一個值,Map按鍵存儲和訪問值,鍵不能重複,即一個鍵只會存儲一份,給同一個鍵重複設置,會覆蓋原來的值。

Set 接口
Set接口,表示的數學中的集合概念,即沒有重複的元素集合。

public interface Map<K,V>{
    V put(K key ,V value)   // 保存鍵值對,如果原來有key,覆蓋,返回原來的值
    
    V get(Object key);  // 根據鍵獲取值,沒找到,返回null
    
    V remove(Object key) //根據鍵刪除鍵值對,返回key原來的值,如果不存在,返回null
    
    int size();  // 查看Map中鍵值對的個數
    
    boolean isEmpty(); //是否爲空
    
    boolean containsKey(Object key); //查看是否包含某一個鍵
    
    boolean containsValue(Object value);// 查看是否包含某個值
    
    void putAll(Map<? extends k,?extends V> m); //保存m中的所有鍵值對當前Map
    
    void clear(); //清空所有Map中所有的鍵值對
    
    Set<key> keySet(); //獲取Map中鍵的集合
    
    Collection<V> values(); //獲取Map中的所有值得集合
    
    Set<Map,Entry<K,V> > entrySet; //獲取Map中的所有鍵值對
    
    interface Entry<K,V>{  //嵌套接口,表示一條鍵值對
       K getKey();  // 鍵值對的鍵
       
       V getValue(); //鍵值對的值
       
       V setValue(V value);
       
       boolean equals(Object o);
       
       int hashCode();
    }

  boolean equals(Object o);
  
  int hashCode();
}

實現原理

1.內部組成

HashMap內部的幾個主要的實例變量

transient Entry<K,V>[] table =(Entry<K,V>[]) EMPTY_TABLE;  // Entryl類型的數組,稱爲哈希表或哈希桶,其中的每一個元素指向一個單鏈表,鏈表中的每一個節點表示一個鍵值對。

transient int size;  // size表示實際鍵值對的個數

int threshold;

final float loadFactor;

Entry 是一個內部類,它的實例變量和構造方法如下:

static class Entry<K,V> implements Map.Entry<K,V>{
    final K key;    // 鍵
   
    V value;     // 值
    
    Entry<K,V> next;   //指向下一個Entry 節點
    
    int hash;  // 是key 的hash 值,直接存儲是爲了加快計算。
    
    Entry(int h,K k ,V v, Entry<K,V> n){
         value = v;
         next = n;
         key = k;
         hash = h;
   }

}

table 的初始值爲EMPTY_TABLE,是一個空表,具體定義爲:

static final Entry<?,?>[] EMPTY_TABLE = {};

當添加鍵值對後,table就不是空表了,它會隨着鍵值對的添加進行擴展,擴展的策略類似於ArrayList。添加第一個元素時,默認分配的大小爲16,不過,並不是size大於16時再進行擴展,而是與threshold有關。

threshold 表示閾值,當鍵值對個數size大於等於threshold時考慮進行擴展。
threshold 是怎麼算出來的? 一般而言,threshold 等於table.length 乘以 loadFactor,如果table.length爲16,loadFactor 爲0.75,則threshold爲12.
loadFactor 是負載因子,表示整體上table被佔用的程度,是一個浮點數,默認爲0.75,可以通過構造方法修改。

2.默認構造方法

public HashMap(){
    this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);
}

//  DEFAULT_INITIAL_CAPACITY爲16,DEFAULT_LOAD_FACTOR爲0.75,默認構造方法調用的構造方法主要代碼爲:
public HashMap(int initialCapacity,int loadFactor){
    this.loadFactor = loadFactor;
    threshold = initialCapacity;
}

3.保存鍵值對

public V put(K key , V value){

//爲table分配實際空間
  if(table == EMPTY_TABLE){
     inflateTable(threshold);
  }
  
  private void inflateTable(int toSize){}
int capacity = roundUpPowerOf2(toSize);
threshold = (int)Math.min(capacity*loadFactor,MAXIMUN_CAPACITY+1);
table = new Entry[capacity];
}
  
 //判斷key是否爲null.單獨處理
  if(key == null){
    return putForNullkey(value)
   }
   
   int hash = hash(key);
   
   // 基於key自身的hashCode() 又進行了一些位運算,目的是爲了隨機和均勻性
final int hash(Object k){
int h = 0;

h ^=k.hashCode();

h^=(h>>>20) ^ (h >>>12)

return h^(h>>>7) ^(h >>>4);
}


 //計算將這個鍵值對放到table的那個位置
  int i = indexFor(hash,table.length);
 
 // HashMap中,length爲2的冪次方, h&(length-1)等同於求模運算h%length,找到了保存位置i,table[i]指向一個單鏈表
static int indexFor(int h,int length){
   rteurn h & (length -1)
}
 
 //就是在這個鏈表中逐個查找是否已經有這個鍵,出現key相同的情況時,用新值替換舊值
  for(Entry<K,V> e = table[i]; e != null;e=e.next ){
   Object k;
   
       //先比較hash,爲什麼?因爲hashs是整數,比較的性能一般要比equals高很多,hash不同,就沒有必要調用equals(),整體上提高性能。
       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,就在給定的位置添加一條
   addEntry(hash, key, value, i);
   
   return null;
}

void addEntry(int hash, k key, V value, int ketIndex ){
       //如果空間不夠,擴容
       if(size >= threshold && (null != table[ketIndex])){
        
        //擴展策略是乘2
        resize(2 * table.length);
        
        hash = (null != key)? hash(key):0;
        
        bucketIndex = indexFor(hash, table.length);
       }
      
     //如果空間是夠的
     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++;
}
 
 //分配一個容量爲原來兩倍的數組,調用transfer方法將原來的鍵值對移植過來,然後更新內部的table變量,以及threshold的值。
void resize(int newCapacity){
   Entry[] oldTable = table;
   int oldCapacity = oldTable.length;
   Entry[] newTable = new Entry[newCapacity];
   transfer(newTable, initHashSeedAsNeeded(newCapacity));
   table = newTable;
   threshold =(int) Math.min(newCapacity * loadFactor, MAXINUM_CAPACITY + 1);
}

//參數rehash一般爲false.遍歷每個鍵值對,計算新位置,並保存到新位置
void transfer(Entry[] newTable, boolean rehash){
     int newCapacity = newTable.length;
     
     for(Entry<K, V> e : table){
        while(null != e){
            Entry<K, V> next = e.next;
               if(rehash){
                   e.hash = null ==e.key ? 0 :hash(e.key);
               }
               int i  = indexFor(e.hash, newCapacity);
               e.next = newTable[i];
               newTable[i] = e;
               e = next;
         }
     
     }
}

4.實例

1.  Map<String, Integer> countMap = new HashMap<>();
2.  countMap.put("hello", 1);
3.  countMap.put("world", 3);
4.  countMap.put("position", 4);

第一行代碼示意圖:
在這裏插入圖片描述

第二行代碼, “hello” 的hash 值爲96207088,模16的結果爲0,所以插入table[0] 指向的鏈表頭部。
在這裏插入圖片描述

第三行代碼 "world"hash值爲111207038,模結果爲14.
在這裏插入圖片描述

第四行代碼 “ position” 模結果爲0
在這裏插入圖片描述

5.查找方法

public  V get(Object key){
  if(key == null){
    return getForNullKey();
 }
 
 Entry<K, V> entry = getEntry(key);
 
 return null == entry ? null : entry.getValue();
}

HashMap 支持key爲null,key爲null 的時間,放在table[0],調用getForNullKey() 獲取值,如果key不爲null,則調用getEntry()獲取鍵值對節點 entry,然後調用節點的getValue()方法獲取值。

final Entry<K, V> geyEntry(Object key){
 if(size == 0){
   return null;
 }
 
 int hash=(key == null) ? 0 : hash(key);
 

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;
}

public boolean containsValue(Object value){
 if(value == null)
    return containsNullValue();
    
  Entry[] tab = table;
  
  // 從table的第一個鏈表開始,從上到下,從左到右逐個節點進行訪問。
  for(int i = 0; i < tab.length; i++){
       for(Entry e = tab[i] ; e != null ; e = e.next ){
     If(value.equals(e.value)){
      return true;
      }}}
      
    return false;  
}

6.根據鍵刪除鍵值對

public V remove(Object key){
    Entry<K, V> e = removeEntryForKey(key);
    
    return(e == null ? null : e.value);
}


final Entry<K, V> removeEntryForKey(Object key){
 if(size == 0){
   return null;
 }
 
 //計算hash,根據hash找到對應的table索引
int hash=(key == null) ? 0 : hash(key);
int i = indexFor(hash,table.length)

// 遍歷table[i],查找待刪除節點,使用變量prev指向前一個節點,next指向後一個節點。e指向當前節點
Entry<K, V> prev = table[i];
Entry<K, V> e = prev;
while(e != null){
  Entry<K, V> next = e.next;
  
  Object k;
  // 判斷是否找到,依然是先比較hash值,hash相同時,再用equals比較
  if(e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
    modCount++;
    
    //刪除的邏輯就是讓長度減小。然後讓待刪節點的前後節點鏈起來,如果待刪節點是第一個節點,則讓table[i]直接指向後一個節點。
    size--;
    if(prev == e)
     table[i] = next
     else
     prev.next = next;
     
     e.recordRemoval(this);
     
     return e;
  }
  
  prev = e;
  e = next;
}

}

7.原理小結

其實HashMap內部有一個哈希表,即數組table,每個元素table[i]指向一個單向鏈表,根據鍵存取值,用鍵算出hash,取模得到數組中索引位置buketIndex,然後操作table[buketIndex]指向單向鏈表。
存取的時候根據鍵的hash值,只有對應鏈表中操作,不會訪問別的鏈表,在對應鏈表操作時也是先比較hash值,如果相同再用equals方法比較。這就要求,相同的對象其hashCode返回值必須相同。

有以下特點:
(1) 根據鍵保存和獲取值得效率都很高,爲O(1);
(2) HashMap 中的鍵值對沒有順序,是隨機的
(3)HashMap 不是線程安全的,HashTable實現原理與HashMap類似,但沒有特別的優化,它內部通過Synchronized實現勒線程安全。
(4)在HashMap 中,鍵和值都可以爲null,而在Hashtable不可以。

8.參考文章

java編程的邏輯基礎(馬俊昌)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章