Map--HashTable
上一篇文章中,我們分析了HashMap的源碼,這一篇文章我們學習Map接口的另一個實現類---HashTable,在學習之前,不熟悉hashMap的可以先看我的上一篇文章Map--HashMap,我們需要先了解下它和HashMap有哪些異同點。
不同點 | HashMap | HashTable |
---|---|---|
繼承的父類 | Dictionary類 | AbstractMap |
線程安全性 | 線程不安全 | 線程安全 |
key和value | 允許爲null | 不允許爲null |
遍歷方式 | Iterator | Iterator和Enumeration |
hash值計算 | 重新計算key的hash值 | 直接使用key的hashCode() |
初始化 | 默認容量爲16 | 默認容量爲11 |
擴容方式 | 原容量*2 | 原容量*2+1 |
數據結構 | 數組+鏈表+紅黑樹 | 數組+鏈表 |
Iterator遍歷數組的順序 | 索引從小到大 | 索引從大到小 |
確認key在數組中的索引 | i=(n-1)&hash | index=(hash&0x7FFFFFFF)%tab.length |
底層數組容量爲2的整數冪 | 一定要爲2的整數冪 | 不要求 |
NOTE:HashMap和HashTable最大的不同體現在線程安全、key和value是否爲null,HashTable是個過時的集合類,如果使用場景不需要線程安全,可以直接使用hashMap來代替;如果需要在線程安全的場景中使用,可以使用ConcurrentHashMap替換,看起來HashTable好像沒什麼用,但是面試經常問啊,所以我們還是需要了解下。
因爲HashMap和HashTable在存儲結構和實現方式上很相似,所以這篇文章主要講解HashTable與HashMap不同的知識點。
我們先看一下HashTable的底層數據結構:
HashTable的數據結構
1、成員屬性
transient Entry[] table:Entry[ ]數組類型,每個Entry代表一個鍵值對
transient int count:HashTable內鍵值對的數量,不是容器的大小
int threshold:調整hashTable容量的閾值
float loadFactor:加載因子
transient int modCount:標記HashTable修改的次數
//Entry[ ]數組類型,每個Entry代表一個鍵值對
private transient Entry<?,?>[] table;
//HashTable內鍵值對的數量,不是容器的大小
private transient int count;
//調整hashTable容量的閾值
private int threshold;
//加載因子
private float loadFactor;
//標記HashTable修改的次數
private transient int modCount = 0;
2、構造函數
HashTable有四個構造函數,我們來逐一分析
-
HashTable(int initialCapacity, float loadFactor)
public Hashtable(int initialCapacity, float loadFactor) {
//初始容量<0,拋出異常
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
//負載因子爲非負整數,否則拋出異常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
//初始化hashTable中的參數、loadFactor、table和threshold
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
//選用initialCapacity*loadFactor和Max_ARRAY_Size+1最小的作爲閾值
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
MAX_ARRAY_SIZE表示爲給數組(Table)分配的最大容量,
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
有同學可能會對MAX_ARRAY_SIZE取值有疑問,爲什麼是Integer.MAX_VALUE-8,這是因爲數組作爲一個對象,需要一塊內存來存儲對象頭信息,對象頭信息的最大佔用內存不能超過8個字節,所以需要減去這個頭信息纔是分配給Table的最大容量。
-
HashTable (int initialCapacity)
以給定的初始容量和麼默認加載因子(0.75f)構造hashtable
public Hashtable(int initialCapacity) {
this(initialCapacity, 0.75f);
}
-
HashTable()
以默認的初始容量(11)和加載因子(0.75f)構造HashTable
public Hashtable() {
this(11, 0.75f);
}
-
HashTable(Map<? extends K, ? extends V> t)
使用給定的鍵值對集合t來構造HashTable
public Hashtable(Map<? extends K, ? extends V> t) {
//初始化HashTable
this(Math.max(2*t.size(), 11), 0.75f);
//將t中的鍵值對插入到HashTable中
putAll(t);
}
我們看一下putAll方法,它內部使用了增強for循環來遍歷,內部調用了put方法,我們在覈心方法講解put方法
public synchronized void putAll(Map<? extends K, ? extends V> t) {
//使用增強for循環來進行遍歷
for (Map.Entry<? extends K, ? extends V> e : t.entrySet())
//調用put方法
put(e.getKey(), e.getValue());
}
3、核心方法
HashTable內部提供了很多方法,我們在這篇文章主要講解HashTable中比較重要的方法,put、get和remove
3.1、put方法
put方法時是將指定的鍵值添加到hashTable中,其添加步驟可以概括爲①判斷value不爲null,爲null時,則拋出異常②計算key的hash值並找到key的索引,獲取key所在位置的entry③遍歷entry,判斷key是否存在④如果key存在,則將用新的值替換舊值⑤如果指定的位置key不存在,直接添加,並返回null
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;
//直接使用hashCode作爲hash值
int hash = key.hashCode();
//找到key的索引位置
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
//獲取指定下標的entry
Entry<K,V> entry = (Entry<K,V>)tab[index];
//遍歷鏈表
for(; entry != null ; entry = entry.next) {
//判斷鏈表中是否有與entry的hash和key相等的對象,如果有,則讓新值覆蓋舊值,並返回舊值
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
//如果在Table沒有對應key值,則新添加一個
addEntry(hash, key, value, index);
return null;
}
在這個程序中,有的同學可能會有疑問,比如key的索引位置是怎麼計算的,添加的流程是什麼,
-
index的計算
hash&0x7FFFFFFF操作是爲了使得hashCode的值爲正數,(計算後的值)%tab.length表示對數組長度取模,從概率上講,採用取模計算可以保證結點在數組上的分配比價均勻,這只是減少哈希衝突一種策略。
如果在Table中對沒有對應的key值,則需要新添加一個,我們具體看一下添加流程
-
addEntry(hash, key, value, index)
addEntry方法的作用是將指定的值的放入到指定座標下,其步驟如下①判斷entry中的個數是否大於閾值②大於閾值時需要擴容並重新計算值的位置③不大於閾值時需要將指定的值放入到index位置下,原有的index位置的元素向後移,可以看出數據的插入是前插
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
//判斷entry的個數是否大於閾值(閾值默認是11*0.75f)
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
//如果超過閾值時,擴容並改變原來元素的位置
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
//將原下標爲index的元素賦值給e
Entry<K,V> e = (Entry<K,V>) tab[index];
//將新的結點放在座標爲index的位置,並將新結點的next設置爲e
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
我們對上面程序中擴容方法rehash還沒有講解,我們來講解下rehash的源碼
-
rehash()
rehash方法是對擴容後的數組重新計算下標值並放入的數組中,其步驟如下①將新的數組的容量擴展爲(原容量)*2+1,②判斷新數組的容量是否超出了最大容量的限制③超出了容量限制,就將最大容量賦值給新的數組容量④遍歷原有數組中的元素並重新計算新的索引,將值存入到新的數組中
protected void rehash() {
int oldCapacity = table.length;
//將原來的數組賦值給oldMap
Entry<?,?>[] oldMap = table;
// overflow-conscious code
//新的容量擴容爲:(原有容量)*2+1
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;
}
//以新容量的大小構造新的容器
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
//根據新容量重新計算閾值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
//遍歷原來數組
for (int i = oldCapacity ; i-- > 0 ;) {
//遍歷每一個槽
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
//對擴容後的元素重新計算下標
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
3.2、get方法
返回指定key的value,如果不存在,則返回null
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
//計算索引值
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
//根據索引找到數組的位置,根據key找到指定的值,並返回,如果沒有則返回null
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
3.3、remove方法
刪除指定key的鍵值對,其流程如下①根據key找到數組中的索引,獲取key所在的entry②遍歷entry,判斷key是否存在③如果key存在,刪除指定的鍵值對,並返回Value值④如果不存在,返回null
public synchronized V remove(Object key) {
Entry<?,?> tab[] = table;
//計算key在hashtabke中的索引
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
//根據索引得到頭結點鍵值對
Entry<K,V> e = (Entry<K,V>)tab[index];
//遍歷entry,找到key的鍵值對並刪除,返回value值
for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
//如果不存在,則返回null
return null;
}
參考文獻
[1]https://blog.csdn.net/panweiwei1994/article/details/77427010
[2]https://blog.csdn.net/panweiwei1994/article/details/77428710
[3]jdk開發文檔
[4]馬克.艾倫.維斯.數據結構與算法分析
在公衆號回覆success領取獨家整理的學習資源
看了這篇文章,你是否「博學」了
「掃碼關注我」,每天博學一點點。