順序儲存與鏈式儲存的集合-HashMap、HashTable
阿新 • • 發佈:2019-01-29
HashMap,日常最常用的資料結構之一。它是基於雜湊表的 Map 介面的實現,以key-value的形式存在。在HashMap中,key-value總是會當做一個整體來處理,系統會根據hash演算法來來計算key-value的儲存位置,我們總是可以通過key快速地存、取value。下面將通過原始碼分析儲存結構、初始化、插入、查詢、移除等來深入分析Hashmap的實現原理。
(1)hashCode 方法的常規協定,該協定宣告相等物件必須具有相等的雜湊碼。當equals方法被重寫時,通常有必要重寫 hashCode 方法。
(2)但hashCode相等,不一定equals()
0.equal hashcode ==的區別
為了分析HashMap,我們首先應該理解hashCode及equal的區別,如下:== | 記憶體地址比較 |
equal | Object預設記憶體地址比較,一般需要複寫 |
hashcode |
主要用於集合的散列表,Object預設為記憶體地址,一般不用設定,除非作用於雜湊集合。 |
1.儲存結構
HashMapde的儲存結構是採用順序儲存結構及鏈式儲存結構。順序儲存結構儲存著每個連結串列的頭結點。每個Key根據計算bucketindex來確定陣列下標。bucketindex=hash&(length-1)。當bucketindex相同時,插入連結串列頭部。 // Entry是單向連結串列。
// 它是 “HashMap鏈式儲存法”對應的連結串列。
// 它實現了Map.Entry 介面,即實現getKey(), getValue(), setValue(V value), equals(Object o), hashCode()這些函式
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
// 指向下一個節點
Entry<K,V> next;
final int hash;
// 建構函式。
// 輸入引數包括"雜湊值(h)", "鍵(k)", "值(v)", "下一節點(n)"
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 判斷兩個Entry是否相等
// 若兩個Entry的“key”和“value”都相等,則返回true。
// 否則,返回false
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
// 實現hashCode()
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final String toString() {
return getKey() + "=" + getValue();
}
// 當向HashMap中新增元素時,繪呼叫recordAccess()。
// 這裡不做任何處理
void recordAccess(HashMap<K,V> m) {
}
// 當從HashMap中刪除元素時,繪呼叫recordRemoval()。
// 這裡不做任何處理
void recordRemoval(HashMap<K,V> m) {
}
}
2.初始化(載入因子)
HashMap有兩個引數影響其效能:初始容量和載入因子。預設初始容量是16,載入因子是0.75。容量是雜湊表中桶(Entry陣列)的數量,初始容量只是雜湊表在建立時的容量。載入因子是雜湊表在其容量自動增加之前可以達到多滿的一種尺度。當雜湊表中的條目數超出了載入因子與當前容量的乘積時,會呼叫方法將容量翻倍。所以這是時間和空間的矛盾,最後根據自己的業務來設定。// 預設的初始容量(容量為HashMap中槽的數目)是16,且實際容量必須是2的整數次冪。 static final int DEFAULT_INITIAL_CAPACITY = 16; // 最大容量(必須是2的冪且小於2的30次方,傳入容量過大將被這個值替換) static final int MAXIMUM_CAPACITY = 1 << 30; // 預設載入因子為0.75 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 指定“容量大小”和“載入因子”的建構函式 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // HashMap的最大容量只能是MAXIMUM_CAPACITY if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //載入因此不能小於0 if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); // 找出“大於initialCapacity”的最小的2的冪 int capacity = 1; while (capacity < initialCapacity) capacity <<= 1; // 設定“載入因子” this.loadFactor = loadFactor; // 設定“HashMap閾值”,當HashMap中儲存資料的數量達到threshold時,就需要將HashMap的容量加倍。 threshold = (int)(capacity * loadFactor); // 建立Entry陣列,用來儲存資料 table = new Entry[capacity]; init(); }
3.bucketindex
HashMap中的資料結構是陣列+單鏈表的組合,我們希望的是元素存放的更均勻,最理想的效果是,Entry陣列中每個位置都只有一個元素,這樣,查詢的時候效率最高,不需要遍歷單鏈表,也不需要通過equals去比較K,而且空間利用率最大。所以可以採用%的方式,既雜湊值%容量=bucketIndex。而原始碼的實現採用 h & (length-1),具有更高的效率。這裡注意,為什麼HashMap的預設容量要求2N次方。 當容量一定是2^n時,h & (length - 1) == h % length4.put
HashMap新增元素主要先根據key的hash計算出bucketindex,如果該buckeindex下標的連結串列存在,則遍歷進行替換,否則往陣列新增新的連結串列。 // 將“key-value”新增到HashMap中
public V put(K key, V value) {
// 若“key為null”,則將該鍵值對新增到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不為null”,則計算該key的雜湊值,然後將其新增到該雜湊值對應的連結串列中。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 若“該key”對應的鍵值對已經存在,則用新的value取代舊的value。然後退出!
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 若“該key”對應的鍵值對不存在,則將“key-value”新增到table中
modCount++;
//將key-value新增到table[i]處
addEntry(hash, key, value, i);
return null;
}
// 返回h在陣列中的索引值,這裡用&代替取模,旨在提升效率
// h & (length-1)保證返回值的小於length
static int indexFor(int h, int length) {
return h & (length-1);
}
// 新增Entry。將“key-value”插入指定位置,bucketIndex是位置索引。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 儲存“bucketIndex”位置的值到“e”中
Entry<K,V> e = table[bucketIndex];
// 設定“bucketIndex”位置的元素為“新Entry”,
// 設定“e”為“新Entry的下一個節點”
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 若HashMap的實際大小 不小於 “閾值”,則調整HashMap的大小
if (size++ >= threshold)
resize(2 * table.length);
}
5.get
HashMap根據key獲取元素主要就是通過bucketindex找到連結串列,進行查詢。 // 獲取key對應的value
public V get(Object key) {
if (key == null)
return getForNullKey();
// 獲取key的hash值
int hash = hash(key.hashCode());
// 在“該hash值對應的連結串列”上查詢“鍵值等於key”的元素
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//判斷key是否相同
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
//沒找到則返回null
return null;
}
6.remove
// 刪除“鍵為key”的元素
final Entry<K,V> removeEntryForKey(Object key) {
// 獲取雜湊值。若key為null,則雜湊值為0;否則呼叫hash()進行計算
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
// 刪除連結串列中“鍵為key”的元素
// 本質是“刪除單向連結串列中的節點”
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}