HashMap
特點
- 基於Hash表的Map接口實現
- 線程非安全,並且允許key與value都爲null值,HashTable與之相反,爲線程安全,key與value都不允許null值。
- 不保證映射的順序,特別是它不保證順序恆久不變,resize時會重排
- 當數組沒有鏈表存在時,HashMap性能最好爲O(1)。而最差爲O(threshould)即所有元素存在一個鏈表上。
- hashMap會根據存入的size和負載英子*數組初始容量進行擴容
數據結構
HashMap實際上是一個鏈表散列的數據結構,即數組和鏈表的結合體,HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表
jdk8優化,如果數組某個位置鏈表長度超過8個會轉成紅黑樹,jdk8之前一直是鏈表,鏈表查詢的複雜度是O(n),而紅黑樹由於其自身的特點,查詢的複雜度是O(log(n)),會提高遍歷性能
源碼解析
構造函數初始化
1、initialCapacity:數組的初始化長度,2的n次方
2、loadFactor:負載英子,默認是0.75
3、threshold:最大容量,threshold=initialCapacity*loadFactor,當entry的數量超過capacity*loadFactor時,容器將自動擴容並重新哈希。
4、在對HashMap進行迭代時,需要遍歷整個table以及後面跟的衝突鏈表。因此對於迭代比較頻繁的場景,不宜將HashMap的初始大小設的過大。
5、增大負載因子可以減少數組所佔用的內存空間,但會增加查詢數據的時間開銷,而查詢是最頻繁的的操作(HashMap 的 get() 與 put()方法都要用到查詢);減小負載因子會提高數據查詢的性能,但會增加Hash表所佔用的內存空間。
6、將對向放入到HashMap或HashSet中時,有兩個方法需要特別關心:hashCode()和equals()。hashCode()方法決定了對象會被放到哪個bucket裏,當多個對象的哈希值衝突時,equals()方法決定了這些對象是否是“同一個對象”。所以,如果要將自定義的對象放入到HashMap或HashSet中,需要@Override hashCode()和equals()方法。
// 以指定初始化容量、負載因子創建 HashMap
public HashMap(int initialCapacity, float loadFactor)
{
// 初始容量不能爲負數
if (initialCapacity < 0)
throw new IllegalArgumentException(
"Illegal initial capacity: " +
initialCapacity);
// 如果初始容量大於最大容量,table數組長度爲最大長度
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 負載因子必須大於 0 的數值
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException(
loadFactor);
// 計算出大於 initialCapacity 的最小的 2 的 n 次方值。
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
// 設置容量極限等於容量 * 負載因子
threshold = (int)(capacity * loadFactor);
// 底層是一個數組
table = new Entry[capacity];
init();
}
//
Entry對象,實現了單向鏈表
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;//key
V value;//值
Entry<K,V> next;//next指針
final int hash;//key的hash值
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
//重寫了hashCode方法
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
}
- put方法
public V put(K key, V value) {
// HashMap允許存放null鍵和null值。
// 當key爲null時,調用putForNullKey方法,將value放置在數組第一個位置。
if (key == null)
return putForNullKey(value);
// 根據key的keyCode重新計算hash值。
int hash = hash(key.hashCode());
// 搜索指定hash值在對應table中的索引。
int i = indexFor(hash, table.length);
// 如果 i 索引處的 Entry 不爲 null,通過循環不斷遍歷 e 元素的下一個元素。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 如果i索引處的Entry爲null,表明此處還沒有Entry。
modCount++;
// 將key、value添加到i索引處。
addEntry(hash, key, value, i);
return null;
}
處理key值爲空的情況
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
//LinkedHashMap 有不同的處理
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
根據key的hash值進行hash運算,如果key的hash值一樣,調用HashMap的hash運算後得到的結果是一樣的
static int hash(int h)
{
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
HashMap對key值得hash值運算後獲取table數組的下標table數組的長度總是2的n次方,默認是16,通過 & 運算得到的就是數組的下標,最大不會超過數組的長度,保證數組長度總是2的n次方,這樣長度-1後二進制所有位置都是1,跟key的hash值進行&運算時會更加均勻的分佈entry到數組上。
static int indexFor(int h, int length)
{
return h & (length-1);
}
將entry存入數組中,如果數組該位置上已經存放有其他元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上
void addEntry(int hash, K key, V value, int bucketIndex) {
//獲取數組當前位置的entry
Entry<K,V> e = table[bucketIndex];
//將要保存的entry存入數組當前位置,之前的entry的指針作爲當前entry的一個next屬性
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
//size+1並且和允許最大比較,如果超過則擴展數組的長度爲之前的2倍
if (size++ >= threshold)
resize(2 * table.length);
}
擴展數組的長度,每次擴展原數組長度的2倍,保證數組的長度爲2的n次方,擴展的實質是創建一個2倍長度的新的數組,原數組中的entry必須重新計算其在新數組中的位置,並放進去,這就是resize。
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//如果已經是所支持的最大長度,設置最大存儲的entry數量爲Interger的最大值
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//創建新的
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
//原數組上entry通過hash計算存放到新的數組上面
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
- get方法
public V get(Object key) {
if (key == null)
return getForNullKey();
//計算hash值
int hash = hash(key.hashCode());
//遍歷數組
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
//比較key值得hash屬性,相同的hash代表一個key
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
//單獨處理key爲空的情況
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
- remove方法
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
final Entry<K,V> removeEntryForKey(Object key) {
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;
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++;
//長度-1
size--;
//操作鏈表
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
- 迭代器實現
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) {
//遍歷table直到找到第一個元素,指向next
do {} while (index < t.length && (next = t[index++]) ==null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//current用來記錄上一個,刪除的時候用
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) ==null);
}
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
//調用hashMap的方法刪除
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
HashSet
hashSet是對HashMap的簡單包裝,對HashSet的函數調用都會轉換成合適的HashMap方法,因此HashSet的實現非常簡單,只有不到300行代碼。這裏不再贅述。
//HashSet是對HashMap的簡單包裝
public class HashSet<E>
{
......
private transient HashMap<E,Object> map;//HashSet裏面有一個HashMap
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
......
public boolean add(E e) {//簡單的方法轉換
return map.put(e, PRESENT)==null;
}
......
}