Java集合框架之Map接口(上)

Map接口主要藉助了hash的思想,以hash表鍵值對的形式存儲,鍵用於hash定位,具有極高的效率。其接口主要實現類如下:

Map
├Hashtable(基本同hashMap,默認爲11,只不過hashtable爲線程安全的,不允許有null值,put, get 都加鎖)
├HashMap(Entry鏈表+數組,默認容量爲16,負載因子爲0.75;長度大於n*16*0.75則容量增大一倍)
└LinkedHashMap(底層爲hashMap的Entry雙向鏈表,繼承自hashMap)
└TreeMap(底層爲紅黑樹實現,繼承自AbstractMap,而AbstractMap又實現了Map接口)
└ LinkedHashMap(底層爲hash表和雙鏈表表)
├concurrentHashMap(採用鎖分離 來保證大併發的效率,Segment數組結構和HashEntry數組結構組成,table[]--hashTable  ,segments[]--table;put加鎖,get不加鎖)

一、Map接口常見實現類介紹

    Map底層數據結構爲哈希表,用Entry數組表示,Entry數據結構如下:

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

protected Entry(int hash, K key, V value, Entry<K,V> next) {
   this.hash = hash;
   this.key = key;
   this.value = value;
   this.next = next;
}

...

}

Entry數組存儲示意圖如下:



        在存儲entry時,根據key的hash值定位到Entry數組的相應位置,如果該位置沒有元素則直接存入,若該位置有元素則將entry鏈接至以該位置元素爲表頭的鏈表(JDK1.8中此處做了優化,當鏈表長度超過一定長度時會轉變爲紅黑樹存儲)。在Entry同一個位置的entry具有相同的key哈希值。在查找相應的元素時,首先計算該entry對象key屬性的哈希值,然後根據其hash值定位到Entry數組相應的位置,然後遍歷鏈表並比較entry對象,直到找到相等值。

       Entry數組(哈希表、散列表)的容量是可變的,在初始化時有初始化大小initialCapacity 和負載因子load factor。當數組容量達到Entry.leng*load factor時,會重新分配一個容量爲原來兩倍的Entry數組,並將原來Entry數組中的元素重新hash至新的Entry數組。負載因子是時間和空間上的一種折中,負載因子越大表示散列表填充程度越大,反之越小。散列表填充程度越大,發生元素碰撞的概率越大,鏈表長度也就越長,查找元素時也就越慢。增大負載因子可以減少散列表所佔空間,但會增加查詢數據的時間開銷(put/get均會用到查詢);減小負載因子會提高數據查詢的性能,但會增加散列表所佔用的存儲空間。

     一般情況下load factor默認爲0.75,可以根據 實際需要適當地調整 load factor 的值;如果程序比較關心空間開銷、內存比較緊張,可以適當地增加負載因子;如果程序比較關心時間開銷,內存比較寬裕則可以適當的減少負載因子。通常情況下,程序員無需改變負載因子的值。 


    1、Hashtable繼承自抽象類Dictionary,並實現Map接口

   默認大小爲11、線程安全(對Entry數組的操作加鎖synchronized)、key-value不允許爲null、擴容爲2*n+1、散列方法是(hash & 0x7FFFFFFF) % tab.length

  a、put操作

     先根據entry的key哈希值定位到散列表的相應位置,如果該位置具有相同key的元素直接覆蓋,如果散列表達到容量極限需要擴容並重新哈希原來的散列表,最後把待插入的entry放入的到相應的位置。

    其源碼如下:

   public synchronized V put(K key, V value) {
// Make sure the value is not null
      if (value == null) {
      throw new NullPointerException();
  }
     // Makes sure the key is not already in the hashtable.
     Entry tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;//根據key值哈希定位
    for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
         if ((e.hash == hash) && e.key.equals(key)) {//key已經存在則直接覆蓋
             V old = e.value;
            e.value = value;
           return old;
     }
 }
     modCount++;
    if (count >= threshold) {//容量達到閾值則擴容極限重新哈希
       // Rehash the table if the threshold is exceeded
          rehash();
              tab = table;
             index = (hash & 0x7FFFFFFF) % tab.length;
}
      // Creates the new entry.
      Entry<K,V> e = tab[index];//取出表頭元素
      tab[index] = new Entry<K,V>(hash, key, value, e);//將元素插入作爲新的表頭
      count++;
      return null;
      }

   hastable擴容:

   protected void rehash() {
       int oldCapacity = table.length;
      Entry[] oldMap = table;//老的散列表
      int newCapacity = oldCapacity * 2 + 1;//爲保證散列效果,表長度爲奇數
      Entry[] newMap = new Entry[newCapacity];.//新的散列表,容量爲原來的兩倍
     modCount++;
     threshold = (int)(newCapacity * loadFactor);//擴容閾值
     table = newMap;//原來的table引用指向新的散列表
      //重新hash老的散列表,並將其插入到新的散列表中
     for (int i = oldCapacity ; i-- > 0 ;) {
         for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
              Entry<K,V> e = old;
             old = old.next;
            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = newMap[index];
           newMap[index] = e;
        }
    }
      }

   原來的散列表仍然保存,能夠保證在擴容時,其他線程正常訪問散列表。


    b、get操作

    先根據key定位到相應的列表,然後遍歷列表,找不到返回null

    public synchronized V get(Object key) {
            Entry tab[] = table;
           int hash = key.hashCode();
           int index = (hash & 0x7FFFFFFF) % tab.length;//根據key的哈希值定位
           for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
              if ((e.hash == hash) && e.key.equals(key)) {//遍歷列表並比較
              return e.value;
              }
          }
           return null;
    }

    2、HashMap繼承自抽象類AbstractMap,並實現Map接口

      默認大小爲16、線程非安全、允許key-value爲null、擴容爲2^n、二次散列hash&(length-1)

     初始化大小爲第一個大於給定值並且爲2^n的整數,如果給定大小爲20,那麼初始化大小爲32。初始化源碼如下:

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

        // Find a power of 2 >= initialCapacity找到第一個大於給定值並且爲2^n的整數,爲了便於散列,且在定位時低位跟1做位與
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
        this.loadFactor = loadFactor;
        threshold = (int)(capacity * loadFactor);
        table = new Entry[capacity];
        init(); //回調函數,子類實現
    }


    a、put操作

      跟hashtable操作基本類似,散列方式不同、擴容方式不同。根據key的hash值找到散列表中的索引後,會循環遍歷table[i]所在鏈表,若找到已存在key值則直接覆蓋,如不存在則通過addEntry添加新對象至鏈表頭部。

  public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());//二次散列使得散列更加均勻
        int i = indexFor(hash, table.length);//根據散列定位

     //若i處索引不爲null,通過循環不斷遍歷e的下一個元素
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;

         //有相同key則覆蓋
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);//回調函數,子類實現
                return oldValue;
            }
        }
      //能執行到此處,說明兩點1、i處索引爲空,2、遍歷完鏈表沒有找到與key相同的值
        modCount++;
        addEntry(hash, key, value, i);//將key、value 添加到索引i處

        return null;
    }

   二次散列:

   static int hash(int h) {
        // This function ensures that hashCodes that differ only by
        // constant multiples at each bit position have a bounded
        // number of collisions (approximately 8 at default load factor).
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
     }

    根據哈希值找到索引:

   static int indexFor(int h, int length) {
        return h & (length-1);//h每一位跟1做與操作,極快
    }

    添加元素,先添加再擴容

    void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if (size++ >= threshold)
            resize(2 * table.length);//擴容爲原來的兩倍
     }

   擴容: 

  void resize(int newCapacity) {
         Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
      }

    重新hash:

    void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;//原來的散列表直接賦值null
                do {
                    Entry<K,V> next = e.next;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
      }

    如果在resize過程中,有其他線程視圖調用get遍歷數據會在成錯誤。


hashMap是輕量級的hashTable,主要在hash值和散列定位方面做了優化

hashTable 默認大小爲何是11?
hashtable默認大小是11是因爲除(近似)質數求餘的分散效果好:
Hashtable的擴容是這樣做的:
        int oldCapacity = table.length;
        int newCapacity = oldCapacity * 2 + 1;
雖然不保證capacity是一個質數,但至少保證它是一個奇數。

Hashtable的尋址是這樣做的:
Entrytab[]=table;inthash=key.hashCode();intindex=(hash&0x7FFFFFFF)%tab.length;
直接用key的hashCode(),不像HashMap裏爲了增強hash的分散效果而要做二次hash

hashMap 與hashtable區別:    父類不同,線程安全性、hash值、默認長度,擴容大小,null
1、二者繼承自不同的類,hashMap繼承自AbstractMap ,hashTable繼承自Dictionary,但二者都實現了Map接口
2、hashtable是線程安全的
3、二者的散列表長度取法不一樣。hashMap默認是16,長度是2^n。hashTable 默認長度爲11,且長度是自定義的init*2增長
4、二者的在散列表中的定位不同,hashMap是自定義hash值之後hash&(length-1), hashTable是直接取hashcode然後(hashcode&0X7FFFFFFF)%length
5、hashtable不允許key-Value爲null
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章