hashmap的擴容因子是0.75 原因 參考:HashMap默認加載因子為什么選擇0.75?(阿里)
Hashtable 是一個很常見的數據結構類型,前段時間阿里的面試官說只要搞懂了HashTable,hashMap,HashSet,treeMap,treeSet這幾個數據結構,阿里的數據結構面試沒問題。
一查才發現,這里面的知識確實不少,都很經典,因此做一個專題
通過此文章,可以了解到一下內容(我去美團,京東,阿里基本每次都問這幾個問題)
(1) Hashtable的存儲結構 (數組+鏈表)
(2)Hashtable的擴容原理,擴容因子0.75,bucket的初始大小11.(擴容的函數為2N+1,hashMap的擴容函數是2N,之所以是2的倍數,是因為,Hashtable為了保證速度,擴容直接位移<<1這樣就是2的倍數)
(3)添加,查找操作的深層次原理,
(4)搜素的幾種方法,以及為什么會產生這幾種搜索方法。
首先總覽一下:
Hashtable與Map關系如下圖:
從圖中可以看出: 
(1) Hashtable繼承於Dictionary類,實現了Map接口。Map是"key-value鍵值對"接口,Dictionary是聲明了操作"鍵值對"函數接口的抽象類。 
(2) Hashtable是通過"拉鏈法"實現的哈希表。它包括幾個重要的成員變量:table, count, threshold, loadFactor, modCount。
  table是一個Entry[]數組類型,而Entry實際上就是一個單向鏈表。哈希表的"key-value鍵值對"都是存儲在Entry數組中的。 
  count是Hashtable的大小,它是Hashtable保存的鍵值對的數量。 
  threshold是Hashtable的閾值,用於判斷是否需要調整Hashtable的容量。threshold的值="容量*加載因子"。
  loadFactor就是加載因子。 
  modCount是用來實現fail-fast機制的  
和HashMap一樣,Hashtable 也是一個散列表,它存儲的內容是鍵值對(key-value)映射, 都是數組+鏈表的形式存儲數據:

定義如下:
public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable { .... public Hashtable() { this(11, 0.75f); } }
由此能看出兩點:
(1)、Hashtable默認 bucket 容量是 11 ,擴容因子是0.75.
也就是說 如果 現在我們創建一個Hashtable,如果里面有8個數值 ,因為:8>=11*0.75;那么,在添加到第8個數值的時候,Hashtable會擴容,
Hashtable 的實例有兩個參數影響其性能:初始容量 和 加載因子。容量 是哈希表中桶 的數量,初始容量 就是哈希表創建時的容量。注意,哈希表的狀態為 open:在發生“哈希沖突”的情況下,單個桶會存儲多個條目,這些條目必須按順序搜索。加載因子 是對哈希表在其容量自動增加之前可以達到多滿的一個尺度。初始容量和加載因子這兩個參數只是對該實現的提示。關於何時以及是否調用 rehash 方法的具體細節則依賴於該實現。通常,默認加載因子是 0.75, 這是在時間和空間成本上尋求一種折衷。加載因子過高雖然減少了空間開銷,但同時也增加了查找某個條目的時間(在大多數 Hashtable 操作中,包括 get 和 put 操作,都反映了這一點)。
這是Hashtable的構造函數:默認初始容量是11,而加載因子是0.75;
protected void rehash() { int oldCapacity = table.length; Entry<?,?>[] oldMap = table; // overflow-conscious code int newCapacity = (oldCapacity << 1) + 1; if (newCapacity - MAX_ARRAY_SIZE > 0) { if (oldCapacity == MAX_ARRAY_SIZE) // Keep running with MAX_ARRAY_SIZE buckets return; newCapacity = MAX_ARRAY_SIZE; }}
紅色的字體表明 Hashtable 擴容的函數是直接左移動1位,並加一,也就是:擴大為原來的2n+1;
(2)、Hashtable 繼承於Dictionary,實現了Map、Cloneable、java.io.Serializable接口。
Hashtable包含的方法 :elements() ,其作用是返回“所有value”的枚舉對象
public synchronized Enumeration<V> elements() { return this.<V>getEnumeration(VALUES); } // 獲取Hashtable的枚舉類對象 private <T> Enumeration<T> getEnumeration(int type) { if (count == 0) { return Collections.emptyEnumeration(); } else { return new Enumerator<>(type, false); } }
從中,我們可以看出:
(1) 若Hashtable的實際大小為0,則返回“空枚舉類”對象emptyEnumerator;
(2) 否則,返回正常的Enumerator的對象。(Enumerator實現了迭代器和枚舉兩個接口,請注意這兩個接口,這是我們后面介紹搜索方法時,會涉及到的)
我們先看看emptyEnumerator對象是如何實現的
private static Enumeration emptyEnumerator = new EmptyEnumerator(); // 空枚舉類 // 當Hashtable的實際大小為0;此時,又要通過Enumeration遍歷Hashtable時,返回的是“空枚舉類”的對象。 private static class EmptyEnumerator implements Enumeration<Object> { EmptyEnumerator() { } // 空枚舉類的hasMoreElements() 始終返回false public boolean hasMoreElements() { return false; } // 空枚舉類的nextElement() 拋出異常 public Object nextElement() { throw new NoSuchElementException("Hashtable Enumerator"); } }
我們在來看看Enumeration類,Enumerator的作用是提供了“通過elements()遍歷Hashtable的接口” 和 “通過entrySet()遍歷Hashtable的接口”。因為,它同時實現了 “Enumerator接口”和“Iterator接口”。
private class Enumerator<T> implements Enumeration<T>, Iterator<T> { Entry<?,?>[] table = Hashtable.this.table; int index = table.length; Entry<?,?> entry; Entry<?,?> lastReturned; int type; .... }
3、以下為Hashtable 包含的函數,函數都是同步的,每個前面都有synchronized,這意味着它是線程安全的。
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;
....
}
由此我們也能看出:Hashtable的key、value都不可以為null。
看源碼:如果value為空 拋出異常,如果 key為空 key.hashCode會拋出異常
我們都知道:Hashtable 的key 和value 都不能為空,HashMap的key 和value 都可以為空,就是這個原因。
此外,Hashtable中的映射不是有序的。
4、 Hashmap一樣,Hashtable也是一個散列表,它也是通過“拉鏈法”解決哈希沖突的。
Hashtable的“拉鏈法”相關內容
Hashtable數據存儲數組,是由一個Entry數組組成的,而 Entry 本身是多個key,value的鏈表,其中鏈表中的每個值都有個next指針,指向本鏈表的下一個元素。
private transient Entry[] table;
Hashtable中的key-value都是存儲在table數組中的。 如下所示,數據節點Entry的數據結構
private static class Entry<K,V> implements Map.Entry<K,V> { // 哈希值 int hash; K key; V value; // 指向的下一個Entry,即鏈表的下一個節點 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; } protected Object clone() { return new Entry<K,V>(hash, key, value, (next==null ? null : (Entry<K,V>) next.clone())); } public K getKey() { return key; } public V getValue() { return value; } // 設置value。若value是null,則拋出異常。 public V setValue(V value) { if (value == null) throw new NullPointerException(); V oldValue = this.value; this.value = value; return oldValue; } // 覆蓋equals()方法,判斷兩個Entry是否相等。 // 若兩個Entry的key和value都相等,則認為它們相等。 public boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; return (key==null ? e.getKey()==null : key.equals(e.getKey())) && (value==null ? e.getValue()==null : value.equals(e.getValue())); } public int hashCode() { return hash ^ (value==null ? 0 : value.hashCode()); } public String toString() { return key.toString()+"="+value.toString(); } }
從中,我們可以看出 Entry 實際上就是一個單向鏈表。這也是為什么我們說Hashtable是通過拉鏈法解決哈希沖突的。
Entry 實現了Map.Entry 接口,即實現getKey(), getValue(), setValue(V value), equals(Object o), hashCode()這些函數。這些都是基本的讀取/修改key、value值的函數。
拿put()方法舉例: put() 的作用是對外提供接口,讓Hashtable對象可以通過put()將“key-value”添加到Hashtable中。
流程大體是先判斷 hash值,然后判斷equals值
PUT流程圖:
如果對hashcode和equals 方法的區別不了解可以參考:Java == ,equals 和 hashcode 的區別和聯系(阿里面試)
put 方法的整個流程為:
- 判斷 value 是否為空,為空則拋出異常;
 - 計算 key 的 hash 值,並根據 hash 值獲得 key 在 table 數組中的位置 index,如果 table[index] 元素不為空,則進行迭代,如果遇到相同的 key,則直接替換,並返回舊 value;
 - 否則,我們可以將其插入到 table[index] 位置。
 
public synchronized V put(K key, V value) {   
 // Hashtable中不能插入value為null的元素!!!                                                                                             
    if (value == null) { throw new NullPointerException(); } // 若“Hashtable中已存在鍵為key的鍵值對”, // 則用“新的value”替換“舊的value” Entry tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { V old = e.value; e.value = value; return old; } } // 若“Hashtable中不存在鍵為key的鍵值對”, // (01) 將“修改統計數”+1 modCount++; // (02) 若“Hashtable實際容量” > “閾值”(閾值=總的容量 * 加載因子) // 則調整Hashtable的大小 if (count >= threshold) { // Rehash the table if the threshold is exceeded  rehash(); tab = table; index = (hash & 0x7FFFFFFF) % tab.length; } // (03) 將“Hashtable中index”位置的Entry(鏈表)保存到e中 Entry<K,V> e = tab[index]; // (04) 創建“新的Entry節點”,並將“新的Entry”插入“Hashtable的index位置”,並設置e為“新的Entry”的下一個元素(即“新Entry”為鏈表表頭)。 tab[index] = new Entry<K,V>(hash, key, value, e); // (05) 將“Hashtable的實際容量”+1 count++; return null; }  
        
通過一個實際的例子來演示一下這個過程:
假設我們現在Hashtable的容量為5,已經存在了(5,5),(13,13),(16,16),(17,17),(21,21)這 5 個鍵值對,目前他們在Hashtable中的位置如下:

現在,我們插入一個新的鍵值對,put(16,22),假設key=16的索引為1.但現在索引1的位置有兩個Entry了,所以程序會對鏈表進行迭代。迭代的過程中,發現其中有一個Entry的key和我們要插入的鍵值對的key相同,所以現在會做的工作就是將newValue=22替換oldValue=16,然后返回oldValue=16.

然后我們現在再插入一個,put(33,33),key=33的索引為3,並且在鏈表中也不存在key=33的Entry,所以將該節點插入鏈表的第一個位置。

再看一下Get()方法,我們知道Hashtable的時間復雜度是O(1),但你知道它是如何通過散列碼的方式做到O(1)的嗎?
Hashtable 直接用hash取了hashtable模,用模做了index,然后定位到bucket桶的數組位置,這個位置上面可能有一個hashcode相同的entry鏈表;然后對這鏈表進行遍歷,找到key等於指定值的entry,因此 時間復雜度為O(1),HashMap,HashTable,HashSet 只要是以Hash為基礎的數據結構都是O(1)
參考:HashMap, HashTable,HashSet,TreeMap 的時間復雜度
get() 的作用就是獲取key對應的value,沒有的話返回null
   public synchronized V get(Object key) {
        Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return (V)e.value; } } return null; } 
        
相比較於 put 方法,get 方法則簡單很多。其過程就是首先通過 hash()方法求得 key 的哈希值,然后根據 hash 值得到 index 索引(上述兩步所用的算法與 put 方法都相同)。然后迭代鏈表,返回匹配的 key 的對應的 value;找不到則返回 null。
5、剛才提到 Hashtable 繼承了 繼承了字典類型:Dictionary類型。而字典類型依賴於: Enumerator
Enumerator實現了方法:Enumeration<T>, Iterator<T>
private class Enumerator<T> implements Enumeration<T>, Iterator<T> { Entry<?,?>[] table = Hashtable.this.table; int index = table.length; Entry<?,?> entry; Entry<?,?> lastReturned; int type; /** * Indicates whether this Enumerator is serving as an Iterator * or an Enumeration. (true -> Iterator). */ boolean iterator;
因此:搜索有五種方法進行搜素:
(1) 利用Iterator迭代器,遍歷Hashtable的鍵值對
第一步:根據entrySet()獲取Hashtable的“鍵值對”的Set集合。
第二步:通過Iterator迭代器遍歷“第一步”得到的集合。   
Iterator iter=table.entrySet().iterator(); while(iter.hasNext()){ Entry entry =(Entry) iter.next(); //獲取key String key=(String)entry.getKey(); Object value=entry.getValue(); System.out.println("key="+key+" value="+value); }
(2) 通過Iterator遍歷Hashtable的鍵
第一步:根據keySet()獲取Hashtable的“鍵”的Set集合。
第二步:通過Iterator迭代器遍歷“第一步”得到的集合。 
// 假設table是Hashtable對象 // table中的key是String類型,value是Integer類型 String key = null; Integer integ = null; Iterator iter = table.keySet().iterator(); while (iter.hasNext()) { // 獲取key key = (String)iter.next(); // 根據key,獲取value integ = (Integer)table.get(key); }
(3)、通過Iterator遍歷Hashtable的值
第一步:根據value()獲取Hashtable的“值”的集合。
第二步:通過Iterator迭代器遍歷“第一步”得到的集合。 
// 假設table是Hashtable對象 // table中的key是String類型,value是Integer類型 Integer value = null; Collection c = table.values(); Iterator iter= c.iterator(); while (iter.hasNext()) { value = (Integer)iter.next(); }
(4)、 通過Enumeration遍歷Hashtable的鍵
第一步:根據keys()獲取Hashtable的集合。
第二步:通過Enumeration遍歷“第一步”得到的集合。
Enumeration enu = table.keys();
while(enu.hasMoreElements()) {
    System.out.println(enu.nextElement());
}    
        (5)、 通過Enumeration遍歷Hashtable的值
第一步:根據elements()獲取Hashtable的集合。
第二步:通過Enumeration遍歷“第一步”得到的集合。
Enumeration enu = table.elements();
while(enu.hasMoreElements()) {
    System.out.println(enu.nextElement());
} 
        遍歷測試程序如下:
import java.util.Collection; import java.util.Enumeration; import java.util.Hashtable; import java.util.Iterator; import java.util.Map.Entry; public class hashtabletest { public static void main(String[] args) { // TODO Auto-generated method stub Hashtable table =new Hashtable(); table.put("張三",20); table.put("李四",30); table.put("王五", 50); // 4.1 遍歷Hashtable的鍵值對 // // 第一步:根據entrySet()獲取Hashtable的“鍵值對”的Set集合。 // 第二步:通過Iterator迭代器遍歷“第一步”得到的集合。 Iterator iter=table.entrySet().iterator(); while(iter.hasNext()){ Entry entry =(Entry) iter.next(); //獲取key String key=(String)entry.getKey(); Object value=entry.getValue(); System.out.println("key="+key+" value="+value); } // //4.2 通過Iterator遍歷Hashtable的鍵 //第一步:根據keySet()獲取Hashtable的“鍵”的Set集合。 //第二步:通過Iterator迭代器遍歷“第一步”得到的集合。 Iterator itkey=table.keySet().iterator(); while(itkey.hasNext()){ String key=(String) itkey.next(); Object value=table.get(key); System.out.println("key=="+key+" value="+value); } // 4.3 通過Iterator遍歷Hashtable的值 // // 第一步:根據value()獲取Hashtable的“值”的集合。 // 第二步:通過Iterator迭代器遍歷“第一步”得到的集合。 Collection c= table.values(); Iterator itvalue=c.iterator(); while(itvalue.hasNext()){ Object value =itvalue.next(); System.out.println(" value="+value); } // 4.4 通過Enumeration遍歷Hashtable的鍵 // // 第一步:根據keys()獲取Hashtable的集合。 // 第二步:通過Enumeration遍歷“第一步”得到的集合。 Enumeration enu=table.keys(); while(enu.hasMoreElements()){ System.out.println("elements="+enu.nextElement()); } // 4.5 通過Enumeration遍歷Hashtable的值 // 第一步:根據elements()獲取Hashtable的集合。 // 第二步:通過Enumeration遍歷“第一步”得到的集合。 Enumeration entry=table.elements(); while(entry.hasMoreElements()){ System.out.println(" element111s ="+entry.nextElement()); } } }
結果為:
key=王五 value=50 key=張三 value=20 key=李四 value=30 key==王五 value=50 key==張三 value=20 key==李四 value=30 value=50 value=20 value=30 elements=王五 elements=張三 elements=李四 element111s =50 element111s =20 element111s =30
6、其他的函數
(1) contains() 和 containsValue()
contains() 和 containsValue() 的作用都是判斷Hashtable是否包含“值(value)”
public boolean containsValue(Object value) {
    return contains(value);
  } 
        
remove() remove() 的作用就是刪除Hashtable中鍵為key的元素
Hashtable實現的Cloneable接口 Hashtable實現了Cloneable接口,即實現了clone()方法。
clone()方法的作用很簡單,就是克隆一個Hashtable對象並返回。
Hashtable實現的Serializable接口,分別實現了串行讀取、寫入功能。
串行寫入函數就是將Hashtable的“總的容量,實際容量,所有的Entry”都寫入到輸出流中
串行讀取函數:根據寫入方式讀出將Hashtable的“總的容量,實際容量,所有的Entry”依次讀出   

