thinking in java (十五) ----- 集合之HahsMap
HashMap介紹
- HashMap簡介
HashMap是一個散列表,儲存的內容是鍵值對對映(key-value)k-v
HashMap繼承於AbstractMap,實現了Map,Cloneable,Serializable介面
HashMap的實現是不同步的(執行緒不安全)
HashMap的例項有倆引數影響效能:“初始容量”和“載入因子”,容量是雜湊表中通的數量,初始容量只是雜湊表在建立時的容量,載入因子是雜湊表在其容量自動增加之前可以達到多滿的尺度。當雜湊表中的條目超出了載入因子與當前容量的乘積時,則要對該雜湊表進行rehash操作(內部重建內部資料結構)從而雜湊表擁有兩倍的桶數,通常預設的載入因子是0.75,這是在時間和空間成本上的一種折中。
- 建構函式
HashMap有四個建構函式
// 預設建構函式。
HashMap()
// 指定“容量大小”的建構函式
HashMap(int capacity)
// 指定“容量大小”和“載入因子”的建構函式
HashMap(int capacity, float loadFactor)
// 包含“子Map”的建構函式
HashMap(Map<? extends K, ? extends V> map)
capacity容量,loaFactor載入因子,預設0.75.
HashMap資料結構
HashMap的繼承關係
java.lang.Object ↳ java.util.AbstractMap<K, V> ↳ java.util.HashMap<K, V> public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { }
HashMap與Map的關係
從圖中可以看出:
1,HashMap繼承與AbstractMap類,實現了Map介面,Map是鍵值對介面,AbstractMap實現了鍵值對的通用函式介面
2,HashMap是通過“拉鍊法”實現的雜湊表,包括幾個重要成員,table,size,threshold,loadFactor,modCount
table是一個Entry[] 陣列型別,而Entry實際上是一個單向連結串列,雜湊表的鍵值對都是儲存在Entry陣列中的
size是HashMap的帶下,它是HashMap實際儲存鍵值對的數量
threshold是HashMap的閾值,用於判斷是否需要調整HashMap的容量,threshold的值=容量 乘以 載入因子。當HashMap中的儲存數量達到了threshold,就需要將HashMap的容量加
loadFactor 載入因子
modCount 用來實現fail-fast機制的??、、?
原始碼解析
說明:我們首先需要了解,HashMap就是一個散列表,他是通過拉鍊法來解決雜湊衝突的,拉鍊法就是把具有相同雜湊地址的關鍵字(同義詞)值放在同一個單鏈表中(自行百度)
HashMap的拉鍊法相關內容
- HashMap資料儲存陣列
transient Entry[] table;
- 資料節點Entry的結構資料
-
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) { } }
從中我們可以看出,Entry實際上是一個單向連結串列,這也是為什麼我們說HashMap是通過拉鍊法解決雜湊衝突的,Entry實現了Map.Entry 介面,就是實現了getKey,getValue,setValue,equals,hashcode等方法,這些都是基本的KV操作方法
-
HashMap的建構函式
一共有四個建構函式
// 預設建構函式。
public HashMap() {
// 設定“載入因子”
this.loadFactor = DEFAULT_LOAD_FACTOR;
// 設定“HashMap閾值”,當HashMap中儲存資料的數量達到threshold時,就需要將HashMap的容量加倍。
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
// 建立Entry陣列,用來儲存資料
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
// 指定“容量大小”和“載入因子”的建構函式
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;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Find a power of 2 >= initialCapacity
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();
}
// 指定“容量大小”的建構函式
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 包含“子Map”的建構函式
public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
// 將m中的全部元素逐個新增到HashMap中
putAllForCreate(m);
}
HashMap主要對外介面
- clear()方法
clear方法的作用是清除HashMap,他是將所有的元素設定為null,
public void clear() {
modCount++;
Entry[] tab = table;
for (int i = 0; i < tab.length; i++)
tab[i] = null;
size = 0;
}
- containKey()方法
判斷HashMap中,是否包含key.
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
containKey()方法先通過getEntry(try)獲取Entry陣列,然後判斷是否為空
- getEntry()方法
final Entry<K,V> getEntry(Object key) {
// 獲取雜湊值
// HashMap將“key為null”的元素儲存在table[0]位置,“key不為null”的則呼叫hash()計算雜湊值
int hash = (key == null) ? 0 : hash(key.hashCode());
// 在“該hash值對應的連結串列”上查詢“鍵值等於key”的元素
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
getEntry的作用就是返回鍵位key的例項,HashMap將鍵為null的值放在table[0]上,key不為null的放在table的其他位置。
- containsValue()方法
判斷HashMap裡面是否含有值為Value的鍵值對
public boolean containsValue(Object value) {
// 若“value為null”,則呼叫containsNullValue()查詢
if (value == null)
return containsNullValue();
// 若“value不為null”,則查詢HashMap中是否有值為value的節點。
Entry[] tab = table;
for (int i = 0; i < tab.length ; i++)
for (Entry e = tab[i] ; e != null ; e = e.next)
if (value.equals(e.value))
return true;
return false;
}
從中,我們可以看出containsNullValue()分為兩步進行處理:第一,若“value為null”,則呼叫containsNullValue()。第二,若“value不為null”,則查詢HashMap中是否有值為value的節點。
- entrySet()、values()、keySet()遍歷
這三個的原始碼大致相似。以entrySet()為例。
// 返回“HashMap的Entry集合”
public Set<Map.Entry<K,V>> entrySet() {
return entrySet0();
}
// 返回“HashMap的Entry集合”,它實際是返回一個EntrySet物件
private Set<Map.Entry<K,V>> entrySet0() {
Set<Map.Entry<K,V>> es = entrySet;
return es != null ? es : (entrySet = new EntrySet());
}
// EntrySet對應的集合
// EntrySet繼承於AbstractSet,說明該集合中沒有重複的EntrySet。
private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public Iterator<Map.Entry<K,V>> iterator() {
return newEntryIterator();
}
public boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<K,V> e = (Map.Entry<K,V>) o;
Entry<K,V> candidate = getEntry(e.getKey());
return candidate != null && candidate.equals(e);
}
public boolean remove(Object o) {
return removeMapping(o) != null;
}
public int size() {
return size;
}
public void clear() {
HashMap.this.clear();
}
}
可以看出entrySet中最主要的方法是 EntryIterator()方法,
// 返回一個“entry迭代器”
Iterator<Map.Entry<K,V>> newEntryIterator() {
return new EntryIterator();
}
// Entry的迭代器
private final class EntryIterator extends HashIterator<Map.Entry<K,V>> {
public Map.Entry<K,V> next() {
return nextEntry();
}
}
// HashIterator是HashMap迭代器的抽象出來的父類,實現了公共了函式。
// 它包含“key迭代器(KeyIterator)”、“Value迭代器(ValueIterator)”和“Entry迭代器(EntryIterator)”3個子類。
private abstract class HashIterator<E> implements Iterator<E> {
// 下一個元素
Entry<K,V> next;
// expectedModCount用於實現fast-fail機制。
int expectedModCount;
// 當前索引
int index;
// 當前元素
Entry<K,V> current;
HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
// 將next指向table中第一個不為null的元素。
// 這裡利用了index的初始值為0,從0開始依次向後遍歷,直到找到不為null的元素就退出迴圈。
while (index < t.length && (next = t[index++]) == null)
;
}
}
public final boolean hasNext() {
return next != null;
}
// 獲取下一個元素
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry<K,V> e = next;
if (e == null)
throw new NoSuchElementException();
// 注意!!!
// 一個Entry就是一個單向連結串列
// 若該Entry的下一個節點不為空,就將next指向下一個節點;
// 否則,將next指向下一個連結串列(也是下一個Entry)的不為null的節點。
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
// 刪除當前元素
public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}
}
我們通過entrySet中的next方法去遍歷HashMap時,實際上呼叫的是nextEntry,而nexrEntry的實現方式,先遍歷Entry(根據Entry在table在table中的序號,從小到大遍歷)
,HashMap由陣列+連結串列組成的,陣列是HashMap的主體,連結串列則是主要為了解決雜湊衝突而存在的,如果定位到的陣列位置不含連結串列(當前entry的next指向null),那麼對於查詢,新增等操作很快,僅需一次定址即可;如果定位到的陣列包含連結串列,對於新增操作,其時間複雜度為O(n),首先遍歷連結串列,存在即覆蓋,否則新增;對於查詢操作來講,仍需遍歷連結串列,然後通過key物件的equals方法逐一比對查詢。所以,效能考慮,HashMap中的連結串列出現越少,效能才會越好。