hashmap的擴容因子是0.75 緣由 參考:HashMap默認加載因子爲何選擇0.75?(阿里)html
Hashtable 是一個很常見的數據結構類型,前段時間阿里的面試官說只要搞懂了HashTable,hashMap,HashSet,treeMap,treeSet這幾個數據結構,阿里的數據結構面試沒問題。java
一查才發現,這裏面的知識確實很多,都很經典,所以作一個專題面試
經過此文章,能夠了解到一下內容(我去美團,京東,阿里基本每次都問這幾個問題)算法
(1) Hashtable的存儲結構 (數組+鏈表)數組
(2)Hashtable的擴容原理,擴容因子0.75,bucket的初始大小11.(擴容的函數爲2N+1,hashMap的擴容函數是2N,之因此是2的倍數,是由於,Hashtable爲了保證速度,擴容直接位移<<1這樣就是2的倍數)安全
(3)添加,查找操做的深層次原理,數據結構
(4)搜素的幾種方法,以及爲何會產生這幾種搜索方法。函數
首先總覽一下:post
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; .... }
三、如下爲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中的映射不是有序的。
四、 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 方法的整個流程爲:
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。
五、剛纔提到 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
六、其餘的函數
(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」依次讀出