1. 程式人生 > >【資料結構】HashTable原理及實現學習總結

【資料結構】HashTable原理及實現學習總結

有兩個類都提供了一個多種用途的hashTable機制,他們都可以將可以key和value結合起來構成鍵值對通過put(key,value)方法儲存起來,然後通過get(key)方法獲取相對應的value值。一個是前面提到的HashMap,還有一個就是馬上要講解的HashTable。對於HashTable而言,它在很大程度上和HashMap的實現差不多,如果我們對HashMap比較瞭解的話,對HashTable的認知會提高很大的幫助。他們兩者之間只存在幾點的不同,這個後面會闡述。

一、定義

        HashTable在Java中的定義如下:

  1. publicclass Hashtable<K,V>  
  2.     extends Dictionary<K,V>  
  3.     implements Map<K,V>, Cloneable, java.io.Serializable  

從中可以看出HashTable繼承Dictionary類,實現Map介面。其中Dictionary類是任何可將鍵對映到相應值的類(如 Hashtable)的抽象父類。每個鍵和每個值都是一個物件。在任何一個 Dictionary 物件中,每個鍵至多與一個值相關聯。Map是"key-value鍵值對"介面。

        HashTable採用"拉鍊法"實現雜湊表,它定義了幾個重要的引數:table、count、threshold、loadFactor、modCount。

        table:為一個Entry[]陣列型別,Entry代表了“拉鍊”的節點,每一個Entry代表了一個鍵值對,雜湊表的"key-value鍵值對"都是儲存在Entry陣列中的。

        count:HashTable的大小,注意這個大小並不是HashTable的容器大小,而是他所包含Entry鍵值對的數量。

        threshold:Hashtable的閾值,用於判斷是否需要調整Hashtable的容量。threshold的值="容量*載入因子"。

        loadFactor:載入因子。

        modCount:用來實現“fail-fast”機制的(也就是快速失敗)。所謂快速失敗就是在併發集合中,其進行迭代操作時,若有其他執行緒對其進行結構性的修改,這時迭代器會立馬感知到,並且立即丟擲ConcurrentModificationException異常,而不是等到迭代完成之後才告訴你(你已經出錯了)。

 二、構造方法

        在HashTabel中存在5個建構函式。通過這5個建構函式我們構建出一個我想要的HashTable。

[java] view plain copy  print?
  1. public Hashtable() {  
  2.         this(110.75f);  
  3.     }  

        預設建構函式,容量為11,載入因子為0.75。

[java] view plain copy  print?
  1. public Hashtable(int initialCapacity) {  
  2.         this(initialCapacity, 0.75f);  
  3.     }  

        用指定初始容量和預設的載入因子 (0.75) 構造一個新的空雜湊表。

[java] view plain copy  print?
  1. public Hashtable(int initialCapacity, float loadFactor) {  
  2.         //驗證初始容量
  3.         if (initialCapacity < 0)  
  4.             thrownew IllegalArgumentException("Illegal Capacity: "+  
  5.                                                initialCapacity);  
  6.         //驗證載入因子
  7.         if (loadFactor <= 0 || Float.isNaN(loadFactor))  
  8.             thrownew IllegalArgumentException("Illegal Load: "+loadFactor);  
  9.         if (initialCapacity==0)  
  10.             initialCapacity = 1;  
  11.         this.loadFactor = loadFactor;  
  12.         //初始化table,獲得大小為initialCapacity的table陣列
  13.         table = new Entry[initialCapacity];  
  14.         //計算閥值
  15.         threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);  
  16.         //初始化HashSeed值
  17.         initHashSeedAsNeeded(initialCapacity);  
  18.     }  

        用指定初始容量和指定載入因子構造一個新的空雜湊表。其中initHashSeedAsNeeded方法用於初始化hashSeed引數,其中hashSeed用於計算key的hash值,它與key的hashCode進行按位異或運算。這個hashSeed是一個與例項相關的隨機值,主要用於解決hash衝突。

[java] view plain copy  print?在CODE上檢視程式碼片派生到我的程式碼片
  1. privateint hash(Object k) {  
  2.         return hashSeed ^ k.hashCode();  
  3.     }  

        構造一個與給定的 Map 具有相同對映關係的新雜湊表。

[java] view plain copy  print?在CODE上檢視程式碼片派生到我的程式碼片
  1. public Hashtable(Map<? extends K, ? extends V> t) {  
  2.         //設定table容器大小,其值==t.size * 2 + 1
  3.         this(Math.max(2*t.size(), 11), 0.75f);  
  4.         putAll(t);  
  5.     }  

三、主要方法

        HashTable的API對外提供了許多方法,這些方法能夠很好幫助我們操作HashTable,但是這裡我只介紹兩個最根本的方法:put、get。

        首先我們先看put方法:將指定 key 對映到此雜湊表中的指定 value。注意這裡鍵key和值value都不可為空。

[java] view plain copy  print?在CODE上檢視程式碼片派生到我的程式碼片
  1. publicsynchronized V put(K key, V value) {  
  2.         // 確保value不為null
  3.         if (value == null) {  
  4.             thrownew NullPointerException();  
  5.         }  
  6.         /* 
  7.          * 確保key在table[]是不重複的 
  8.          * 處理過程: 
  9.          * 1、計算key的hash值,確認在table[]中的索引位置 
  10.          * 2、迭代index索引位置,如果該位置處的連結串列中存在一個一樣的key,則替換其value,返回舊值 
  11.          */
  12.         Entry tab[] = table;  
  13.         int hash = hash(key);    //計算key的hash值
  14.         int index = (hash & 0x7FFFFFFF) % tab.length;     //確認該key的索引位置
  15.         //迭代,尋找該key,替換
  16.         for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {  
  17.             if ((e.hash == hash) && e.key.equals(key)) {  
  18.                 V old = e.value;  
  19.                 e.value = value;  
  20.                 return old;  
  21.             }  
  22.         }  
  23.         modCount++;  
  24.         if (count >= threshold) {  //如果容器中的元素數量已經達到閥值,則進行擴容操作
  25.             rehash();  
  26.             tab = table;  
  27.             hash = hash(key);  
  28.             index = (hash & 0x7FFFFFFF) % tab.length;  
  29.         }  
  30.         // 在索引位置處插入一個新的節點
  31.         Entry<K,V> e = tab[index];  
  32.         tab[index] = new Entry<>(hash, key, value, e);  
  33.         //容器中元素+1
  34.         count++;  
  35.         returnnull;  
  36.     }  

        put方法的整個處理流程是:計算key的hash值,根據hash值獲得key在table陣列中的索引位置,然後迭代該key處的Entry連結串列(我們暫且理解為連結串列),若該連結串列中存在一個這個的key物件,那麼就直接替換其value值即可,否則在將改key-value節點插入該index索引位置處。如下:

        首先我們假設一個容量為5的table,存在8、10、13、16、17、21。他們在table中位置如下:


然後我們插入一個數:put(16,22),key=16在table的索引位置為1,同時在1索引位置有兩個數,程式對該“連結串列”進行迭代,發現存在一個key=16,這時要做的工作就是用newValue=22替換oldValue16,並將oldValue=16返回。


在put(33,33),key=33所在的索引位置為3,並且在該連結串列中也沒有存在某個key=33的節點,所以就將該節點插入該連結串列的第一個位置。

 在HashTabled的put方法中有兩個地方需要注意:

        1、HashTable的擴容操作,在put方法中,如果需要向table[]中新增Entry元素,會首先進行容量校驗,如果容量已經達到了閥值,HashTable就會進行擴容處理rehash(),如下:

[java] view plain copy  print?在CODE上檢視程式碼片派生到我的程式碼片
  1. protectedvoid rehash() {  
  2.         int oldCapacity = table.length;  
  3.         //元素
  4.         Entry<K,V>[] oldMap = table;  
  5.         //新容量=舊容量 * 2 + 1
  6.         int newCapacity = (oldCapacity << 1) + 1;  
  7.         if (newCapacity - MAX_ARRAY_SIZE > 0) {  
  8.             if (oldCapacity == MAX_ARRAY_SIZE)  
  9.                 return;  
  10.             newCapacity = MAX_ARRAY_SIZE;  
  11.         }  
  12.         //新建一個size = newCapacity 的HashTable
  13.         Entry<K,V>[] newMap = new Entry[];  
  14.         modCount++;  
  15.         //重新計算閥值
  16.         threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY