1. 程式人生 > >順序儲存與鏈式儲存的集合-HashMap、HashTable

順序儲存與鏈式儲存的集合-HashMap、HashTable

HashMap,日常最常用的資料結構之一。它是基於雜湊表的 Map 介面的實現,以key-value的形式存在。在HashMap中,key-value總是會當做一個整體來處理,系統會根據hash演算法來來計算key-value的儲存位置,我們總是可以通過key快速地存、取value。下面將通過原始碼分析儲存結構、初始化、插入、查詢、移除等來深入分析Hashmap的實現原理。

0.equal hashcode ==的區別

為了分析HashMap,我們首先應該理解hashCode及equal的區別,如下:
== 記憶體地址比較
equal Object預設記憶體地址比較,一般需要複寫
hashcode
主要用於集合的散列表,Object預設為記憶體地址,一般不用設定,除非作用於雜湊集合。
(1)hashCode 方法的常規協定,該協定宣告相等物件必須具有相等的雜湊碼。當equals方法被重寫時,通常有必要重寫 hashCode 方法。 (2)但hashCode相等,不一定equals()

1.儲存結構

HashMapde的儲存結構是採用順序儲存結構及鏈式儲存結構。順序儲存結構儲存著每個連結串列的頭結點。每個Key根據計算bucketindex來確定陣列下標。bucketindex=hash&(length-1)。當bucketindex相同時,插入連結串列頭部。
  // Entry是單向連結串列。
  // 它是 “HashMap鏈式儲存法”對應的連結串列。
  // 它實現了Map.Entry 介面,即實現getKey(), getValue(), setValue(V value), equals(Object o), hashCode()這些函式
  static class Entry<K,V> implements Map.Entry<K,V> {
  final K key;
  V value;
  // 指向下一個節點
  Entry<K,V> next;
  final int hash;

  // 建構函式。
  // 輸入引數包括"雜湊值(h)", "鍵(k)", "值(v)", "下一節點(n)"
  Entry(int h, K k, V v, Entry<K,V> n) {
  value = v;
  next = n;
  key = k;
  hash = h;
  }

  public final K getKey() {
  return key;
  }

  public final V getValue() {
  return value;
  }

  public final V setValue(V newValue) {
  V oldValue = value;
  value = newValue;
  return oldValue;
  }

  // 判斷兩個Entry是否相等
  // 若兩個Entry的“key”和“value”都相等,則返回true。
  // 否則,返回false
  public final boolean equals(Object o) {
  if (!(o instanceof Map.Entry))
  return false;
  Map.Entry e = (Map.Entry)o;
  Object k1 = getKey();
  Object k2 = e.getKey();
  if (k1 == k2 || (k1 != null && k1.equals(k2))) {
  Object v1 = getValue();
  Object v2 = e.getValue();
  if (v1 == v2 || (v1 != null && v1.equals(v2)))
  return true;
  }
  return false;
  }

  // 實現hashCode()
  public final int hashCode() {
  return (key==null ? 0 : key.hashCode()) ^
  (value==null ? 0 : value.hashCode());
  }

  public final String toString() {
  return getKey() + "=" + getValue();
  }

  // 當向HashMap中新增元素時,繪呼叫recordAccess()。
  // 這裡不做任何處理
  void recordAccess(HashMap<K,V> m) {
  }

  // 當從HashMap中刪除元素時,繪呼叫recordRemoval()。
  // 這裡不做任何處理
  void recordRemoval(HashMap<K,V> m) {
  }
  }

2.初始化(載入因子)

 HashMap有兩個引數影響其效能:初始容量載入因子。預設初始容量是16,載入因子是0.75。容量是雜湊表中桶(Entry陣列)的數量,初始容量只是雜湊表在建立時的容量。載入因子是雜湊表在其容量自動增加之前可以達到多滿的一種尺度。當雜湊表中的條目數超出了載入因子與當前容量的乘積時,會呼叫方法將容量翻倍。所以這是時間和空間的矛盾,最後根據自己的業務來設定。
// 預設的初始容量(容量為HashMap中槽的數目)是16,且實際容量必須是2的整數次冪。    
    static final int DEFAULT_INITIAL_CAPACITY = 16;    
   
    // 最大容量(必須是2的冪且小於2的30次方,傳入容量過大將被這個值替換)    
    static final int MAXIMUM_CAPACITY = 1 << 30;    
   
    // 預設載入因子為0.75   
    static final float DEFAULT_LOAD_FACTOR = 0.75f;    
   // 指定“容量大小”和“載入因子”的建構函式    
    public HashMap(int initialCapacity, float loadFactor) {    
        if (initialCapacity < 0)    
            throw new IllegalArgumentException("Illegal initial capacity: " +    
                                               initialCapacity);    
        // HashMap的最大容量只能是MAXIMUM_CAPACITY    
        if (initialCapacity > MAXIMUM_CAPACITY)    
            initialCapacity = MAXIMUM_CAPACITY;    
        //載入因此不能小於0  
        if (loadFactor <= 0 || Float.isNaN(loadFactor))    
            throw new IllegalArgumentException("Illegal load factor: " +    
                                               loadFactor);    
   
        // 找出“大於initialCapacity”的最小的2的冪    
        int capacity = 1;    
        while (capacity < initialCapacity)    
            capacity <<= 1;    
   
        // 設定“載入因子”    
        this.loadFactor = loadFactor;    
        // 設定“HashMap閾值”,當HashMap中儲存資料的數量達到threshold時,就需要將HashMap的容量加倍。    
        threshold = (int)(capacity * loadFactor);    
        // 建立Entry陣列,用來儲存資料    
        table = new Entry[capacity];    
        init();    
    }    

3.bucketindex

HashMap中的資料結構是陣列+單鏈表的組合,我們希望的是元素存放的更均勻,最理想的效果是,Entry陣列中每個位置都只有一個元素,這樣,查詢的時候效率最高,不需要遍歷單鏈表,也不需要通過equals去比較K,而且空間利用率最大。所以可以採用%的方式,既雜湊值%容量=bucketIndex。而原始碼的實現採用 h & (length-1),具有更高的效率。這裡注意,為什麼HashMap的預設容量要求2N次方。 當容量一定是2^n時,h & (length - 1) == h % length

4.put

     HashMap新增元素主要先根據key的hash計算出bucketindex,如果該buckeindex下標的連結串列存在,則遍歷進行替換,否則往陣列新增新的連結串列。
 // 將“key-value”新增到HashMap中    
    public V put(K key, V value) {    
        // 若“key為null”,則將該鍵值對新增到table[0]中。    
        if (key == null)    
            return putForNullKey(value);    
        // 若“key不為null”,則計算該key的雜湊值,然後將其新增到該雜湊值對應的連結串列中。    
        int hash = hash(key.hashCode());    
        int i = indexFor(hash, table.length);    
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {    
            Object k;    
            // 若“該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;    
            }    
        }    
   
        // 若“該key”對應的鍵值對不存在,則將“key-value”新增到table中    
        modCount++;  
        //將key-value新增到table[i]處  
        addEntry(hash, key, value, i);    
        return null;    
    }    
 
 // 返回h在陣列中的索引值,這裡用&代替取模,旨在提升效率   
    // h & (length-1)保證返回值的小於length    
    static int indexFor(int h, int length) {    
        return h & (length-1);    
    }    

  // 新增Entry。將“key-value”插入指定位置,bucketIndex是位置索引。    
    void addEntry(int hash, K key, V value, int bucketIndex) {    
        // 儲存“bucketIndex”位置的值到“e”中    
        Entry<K,V> e = table[bucketIndex];    
        // 設定“bucketIndex”位置的元素為“新Entry”,    
        // 設定“e”為“新Entry的下一個節點”    
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);    
        // 若HashMap的實際大小 不小於 “閾值”,則調整HashMap的大小    
        if (size++ >= threshold)    
            resize(2 * table.length);    
    }    

5.get

HashMap根據key獲取元素主要就是通過bucketindex找到連結串列,進行查詢。
 // 獲取key對應的value    
    public V get(Object key) {    
        if (key == null)    
            return getForNullKey();    
        // 獲取key的hash值    
        int hash = hash(key.hashCode());    
        // 在“該hash值對應的連結串列”上查詢“鍵值等於key”的元素    
        for (Entry<K,V> e = table[indexFor(hash, table.length)];    
             e != null;    
             e = e.next) {    
            Object k;    
            //判斷key是否相同  
            if (e.hash == hash && ((k = e.key) == key || key.equals(k)))    
                return e.value;    
        }  
        //沒找到則返回null  
        return null;    
    }    

6.remove

  // 刪除“鍵為key”的元素    
    final Entry<K,V> removeEntryForKey(Object key) {    
        // 獲取雜湊值。若key為null,則雜湊值為0;否則呼叫hash()進行計算    
        int hash = (key == null) ? 0 : hash(key.hashCode());    
        int i = indexFor(hash, table.length);    
        Entry<K,V> prev = table[i];    
        Entry<K,V> e = prev;    
   
        // 刪除連結串列中“鍵為key”的元素    
        // 本質是“刪除單向連結串列中的節點”    
        while (e != null) {    
            Entry<K,V> next = e.next;    
            Object k;    
            if (e.hash == hash &&    
                ((k = e.key) == key || (key != null && key.equals(k)))) {    
                modCount++;    
                size--;    
                if (prev == e)    
                    table[i] = next;    
                else   
                    prev.next = next;    
                e.recordRemoval(this);    
                return e;    
            }    
            prev = e;    
            e = next;    
        }    
   
        return e;    
    }    

7.HashTable

(1)HashTable的實現原理基本與HashMap一致,除了細微的方法實現不一致外,所以不進行原理分析。 (2)HashTable為執行緒安全,HashMap為非執行緒安全 (3)HashMap可以接受為null的key和value,而Hashtable則不行 (4)HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以當有其它執行緒改變了HashMap的結構(增加或者移除元素),將會丟擲ConcurrentModificationException,但迭代器本身的remove()方法移除元素則不會丟擲ConcurrentModificationException異常 (5)Java 5提供了ConcurrentHashMap(區域性鎖機制),它是HashTable的替代,比HashTable的擴充套件性、效能更好

8.總結

(1)HashMap是順序結構及連結串列結構的組合 (2)最後根據業務設定容量及載入因子可以提高插入及查詢效率,同時可以避免增容帶來的效率問題 (3)HashMap為非執行緒安全 (4)HashMap的精髓為bucketindex(使元連結串列均勻分佈在陣列),可以提供空間的利用率及元素的插入及查詢效率。 (5)由於hash相等,equal不一定相等。有可能導致撞庫的問題,由於HashMap具有連結串列的功能。可以避免撞庫問題。