JAVA經典集合框架學習筆記——HashMap的底層實現原理


  最近做的幾個項目都是用Map來存儲的數據 ,雖然用得挺順手,但是對HashMap的底層原理卻只知甚少,今天便來簡單學習和整理一下。

  數據結構中有數組和鏈表這兩個結構來存儲數據。

  數組存儲區間是連續的,占用內存嚴重,故空間復雜的很大。但數組的二分查找時間復雜度小,為O(1);數組的特點是:尋址容易,插入和刪除困難;

  鏈表存儲區間離散,占用內存比較寬松,故空間復雜度很小,但時間復雜度很大,達O(N)。鏈表的特點是:尋址困難,插入和刪除容易。

  綜合這兩者的優點,摒棄缺點,哈希表就誕生了,既滿足了數據查找方面的特點,占用的空間也不大。

  哈希表可以說就是數組鏈表,底層還是數組但是這個數組每一項就是一個鏈表。

  在這個數組中,每個元素存儲的其實是一個鏈表的頭,元素的存儲位置一般情況是通過hash(key)%len獲得,也就是元素的key的哈希值對數組長度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存儲在數組下標為12的位置。

  HashMap的構造函數

  HashMap實現了Map接口,繼承AbstractMap。其中Map接口定義了鍵映射到值的規則,而AbstractMap類提供 Map 接口的骨干實現。

  HashMap提供了三個構造函數:

  HashMap():構造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap。

  HashMap(int initialCapacity):構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。

       HashMap(int initialCapacity, float loadFactor):構造一個帶指定初始容量和加載因子的空 HashMap。

 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都會構造一個table數組,而table數組的元素為Entry節點。

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
}

  HashMap也可以說是一個數組鏈表,HashMap里面有一個非常重要的內部靜態類——Entry,這個Entry非常重要,它里面包含了鍵key,值value,下一個節點next,以及hash值,Entry是HashMap非常重要的一個基礎Bean,因為所有的內容都存在Entry里面,HashMap的本質可以理解為 Entry[ ] 數組。

  HashMap.put(key,value)

public V put(K key, V value) {
        //當key為null,調用putForNullKey方法,保存null與table第一個`        位置中,這是HashMap允許為null的原因
        if (key == null)
            return putForNullKey(value);
        //計算key的hash值
        int hash = hash(key.hashCode());                  ------(1)
        //計算key hash 值在 table 數組中的位置
        int i = indexFor(hash, table.length);             ------(2)
        //從i出開始迭代 e,找到 key 保存的位置
        for (Entry<K, V> e = table[i]; e != null; e = e.next) {
            Object k;
            //判斷該條鏈上是否有hash值相同的(key相同)
            //若存在相同,則直接覆蓋value,返回舊value
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;    //舊值 = 新值
                e.value = value;
                e.recordAccess(this);
                return oldValue;     //返回舊值
            }
        }
        //修改次數增加1
        modCount++;
        //將key、value添加至i位置處
        addEntry(hash, key, value, i);
        return null;
    }
 

  我簡單的理解一下,當執行put操作的時候,HashMap會先判斷一下要存儲內容的key值是否為null,如果為null,如果為null,則執行putForNullKey方法,這個方法的作用就是將內容存儲到Entry[]數組的第一個位置,如果key不為null,則去計算key的hash值,然后對數組長度取模,得到要存儲位置的下標,再迭代該數組元素上的鏈表,看該鏈表上是否有hash值相同的,如果有hash值相同的,就直接覆蓋value的值,如果沒有hash值相同的情況,就將該內容存儲到鏈表的表頭,最先儲存的內容會放在鏈表的表尾,其實這帶代碼也順道解釋了HashMap沒有Key值相同的情況。這里還有一個情況也要說明一下,會不會出現鏈表過長的情況?隨着要存儲的內容越來越多,HashMap里面的東西也越來越多,相同下標的情況也增多,那么迭代鏈表的也無疑增加了,這會影響數據的查詢效率,HashMap對此也做了優化,當HashMap中存儲的內容超過數組長度 *loadFactor時,數組就會進行擴容,默認的數組長度是16,loadFactor為加載因子,默認的值為0.75。對於擴容需要說明的一點就是,擴容是一個非常“消耗”的過程,需要重新計算數據在新數組中的位置,並且將內容復制到新數組中,如果我們預先知道HashMap中的元素個數,預設元素的個數,能有效的提高HashMap的存儲效率。

  HashMap.get(key)

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

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

final Entry<K,V> getEntry(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;

    }

  get(key)方法的代碼比較好理解,根據key的hash值找到對應的Entry即鏈表,然后在返回該key值對應的value。

  HashMap的遍歷

public static void main(String[] args) {  
  
  
  Map<String, String> map = new HashMap<String, String>();  
  map.put("1", "value1");  
  map.put("2", "value2");  
  map.put("3", "value3");  
    
  //第一種:普遍使用,二次取值  
  System.out.println("通過Map.keySet遍歷key和value:");  
  for (String key : map.keySet()) {  
   System.out.println("key= "+ key + " and value= " + map.get(key));  
  }  
    
  //第二種  
  System.out.println("通過Map.entrySet使用iterator遍歷key和value:");  
  Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();  
  while (it.hasNext()) {  
   Map.Entry<String, String> entry = it.next();  
   System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());  
  }  
    
  //第三種:推薦,尤其是容量大時  
  System.out.println("通過Map.entrySet遍歷key和value");  
  for (Map.Entry<String, String> entry : map.entrySet()) {  
   System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());  
  }  
  
  //第四種  
  System.out.println("通過Map.values()遍歷所有的value,但不能遍歷key");  
  for (String v : map.values()) {  
   System.out.println("value= " + v);  
  }  
 }   

  使用HashMap的匿名內部類Entry遍歷比使用keySet()效率要高很多,使用forEach循環時要注意不要在循環的過程中改變鍵值對的任何一方的值,否則出現哈希表的值沒有隨着鍵值的改變而改變,到時候在刪除的時候會出現問題。 此外,entrySet比keySet快些。對於keySet其實是遍歷了2次,一次是轉為iterator,一次就從hashmap中取出key所對於的value。而entrySet只是遍歷了第一次,他把key和value都放到了entry中,所以就快了。

     

 


免責聲明!

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



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