Java集合(四)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是一個散列表,儲存的是鍵值對對映。它的實現不是同步的,也就是不是執行緒安全的。它的key-value可以定為null。同時儲存的資料是無序的。
HashMap中有兩個引數影響效能:“初始容量”和“載入因子”。容量是HashMap中桶的數量。初始容量是雜湊表在建立時的容量,載入因子是雜湊表子在容量自動增長之前可以達到多滿的一種尺度。當雜湊表中的實際數量大於載入容量*當前容量時,需要對HashMap進行resize()。
HashMap原始碼分析
HashMap原始碼(JDK6)
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { //預設的初始容量 static final int DEFAULT_INITIAL_CAPACITY = 16; //最大容量,如果傳入容量多大,將被這個值替換 static final int MAXIMUM_CAPACITY = 1 << 30; //預設載入因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; //儲存資料的Entry陣列,長度是2的n次冪。 transient Entry[] table; //HashMap的大小,他是HashMap儲存的鍵值對的數量 transient int size; //閥值,用於判斷是否需要調整HashMap的容量,閥值=(容量*載入因子) int threshold; //載入因子的實際大小 final float loadFactor; //HashMap的改變次數 transient volatile int modCount; //指定容量大小和載入因子的建構函式 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; 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; //設定閥值 threshold = (int)(capacity * loadFactor); //建立Entry陣列,儲存資料 table = new Entry[capacity]; init(); } //指定容量的建構函式 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //預設建構函式 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR); table = new Entry[DEFAULT_INITIAL_CAPACITY]; init(); } //含有子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); putAllForCreate(m); } //計算hash值(擾動函式) static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } //返回陣列下標,根據hash值和HashMap的長度,計算出下標值 static int indexFor(int h, int length) { return h & (length-1); } //返回HashMap的大小 public int size() { return size; } //判斷hashMap是否為空 public boolean isEmpty() { return size == 0; } //獲取key對應的vaule 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; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) return e.value; } return null; } //獲取key為null的元素的值(HashMap將key為Null的元素儲存在table[0]上) private V getForNullKey() { for (Entry<K,V> e = table[0]; e != null; e = e.next) { if (e.key == null) return e.value; } return null; } //hashMap是否包含Key public boolean containsKey(Object key) { return getEntry(key) != null; } //返回鍵為key的鍵值對 final Entry<K,V> getEntry(Object key) { //計算hash值,HashMap將key為Null的元素儲存在table[0]的位置,key不為null的需要計算hash值 int hash = (key == null) ? 0 : hash(key.hashCode()); //在hash值對應的連結串列上查詢鍵值=key的元素,先判斷hash值是否相等,在判斷key是否相等,是因為通過hash判斷的效率高 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; } //將key-value新增到HashMap中 public V put(K key, V value) { //如果key為Null,將鍵值對新增到table[0]的位置 if (key == null) return putForNullKey(value); //key不為空,計算hash值,根據hash值獲取位置索引 int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); //遍歷連結串列,判斷key對應的值是否存在,存在用新值替代 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; } } modCount++; //鍵值對不存在,將key-value新增到table中 addEntry(hash, key, value, i); return null; } //將key為null的新增到table[0]的位置 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; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null; } //建立hashMap對應的新增方法,用於內部呼叫,被建構函式呼叫建立HashMap,put()用於對外提供新增元素 private void putForCreate(K key, V value) { int hash = (key == null) ? 0 : hash(key.hashCode()); int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { e.value = value; return; } } createEntry(hash, key, value, i); } //將m中的元素全都新增到HashMap中 private void putAllForCreate(Map<? extends K, ? extends V> m) { //利用迭代器將元素逐個新增到hashMap中 for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) { Map.Entry<? extends K, ? extends V> e = i.next(); putForCreate(e.getKey(), e.getValue()); } } //重新調整HashMap的大小,newCapacity是調整之後的大小 void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); } //將HashMap中的全部元素都新增到newTable中 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); } } } //將m中的元素全部新增到HashMap中 public void putAll(Map<? extends K, ? extends V> m) { int numKeysToBeAdded = m.size(); if (numKeysToBeAdded == 0) return; if (numKeysToBeAdded > threshold) { int targetCapacity = (int)(numKeysToBeAdded / loadFactor + 1); if (targetCapacity > MAXIMUM_CAPACITY) targetCapacity = MAXIMUM_CAPACITY; int newCapacity = table.length; while (newCapacity < targetCapacity) newCapacity <<= 1; if (newCapacity > table.length) resize(newCapacity); } //通過迭代將m中的元素新增到HashMap中 for (Iterator<? extends Map.Entry<? extends K, ? extends V>> i = m.entrySet().iterator(); i.hasNext(); ) { Map.Entry<? extends K, ? extends V> e = i.next(); put(e.getKey(), e.getValue()); } } //刪除鍵為key的元素 public V remove(Object key) { Entry<K,V> e = removeEntryForKey(key); return (e == null ? null : e.value); } //刪除鍵為key的元素 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++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; } //刪除鍵值對 final Entry<K,V> removeMapping(Object o) { if (!(o instanceof Map.Entry)) return null; Map.Entry<K,V> entry = (Map.Entry<K,V>) o; Object key = entry.getKey(); 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; if (e.hash == hash && e.equals(entry)) { modCount++; size--; if (prev == e) table[i] = next; else prev.next = next; e.recordRemoval(this); return e; } prev = e; e = next; } return e; } //清空HashMap public void clear() { modCount++; Entry[] tab = table; for (int i = 0; i < tab.length; i++) tab[i] = null; size = 0; } //判斷是否包含某值 public boolean containsValue(Object value) { if (value == null) return containsNullValue(); 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; } //判斷是否包含null值 private boolean containsNullValue() { Entry[] tab = table; for (int i = 0; i < tab.length ; i++) for (Entry e = tab[i] ; e != null ; e = e.next) if (e.value == null) return true; return false; } //克隆函式,並返回Object物件 public Object clone() { HashMap<K,V> result = null; try { result = (HashMap<K,V>)super.clone(); } catch (CloneNotSupportedException e) { // assert false; } result.table = new Entry[table.length]; result.entrySet = null; result.modCount = 0; result.size = 0; result.init(); result.putAllForCreate(this); return result; } static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; final int hash; 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; } 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; } public final int hashCode() { return (key==null ? 0 : key.hashCode()) ^ (value==null ? 0 : value.hashCode()); } public final String toString() { return getKey() + "=" + getValue(); } void recordAccess(HashMap<K,V> m) { } void recordRemoval(HashMap<K,V> m) { } } //將key-value新增到指定位置,一般用於新增entry可能會導致hashMap的實際容量超過閥值的情況 void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); //如果實際大小>閥值,調整HashMap的大小 if (size++ >= threshold) resize(2 * table.length); } //建立entry,一般用於新增Entry不會導致hashMap的實際容量超過閥值的情況 void createEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<K,V>(hash, key, value, e); size++; } //HashIterator迭代器是HashMpa抽象出來的父類,實現了公共函式 private abstract class HashIterator<E> implements Iterator<E> { Entry<K,V> next; // next entry to return int expectedModCount; // For fast-fail int index; // current slot Entry<K,V> current; // current entry HashIterator() { expectedModCount = modCount; if (size > 0) { // advance to first entry Entry[] t = table; 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(); 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; } } private final class ValueIterator extends HashIterator<V> { public V next() { return nextEntry().value; } } private final class KeyIterator extends HashIterator<K> { public K next() { return nextEntry().getKey(); } } private final class EntryIterator extends HashIterator<Map.Entry<K,V>> { public Map.Entry<K,V> next() { return nextEntry(); } } Iterator<K> newKeyIterator() { return new KeyIterator(); } Iterator<V> newValueIterator() { return new ValueIterator(); } Iterator<Map.Entry<K,V>> newEntryIterator() { return new EntryIterator(); } private transient Set<Map.Entry<K,V>> entrySet = null; public Set<K> keySet() { Set<K> ks = keySet; return (ks != null ? ks : (keySet = new KeySet())); } private final class KeySet extends AbstractSet<K> { public Iterator<K> iterator() { return newKeyIterator(); } public int size() { return size; } public boolean contains(Object o) { return containsKey(o); } public boolean remove(Object o) { return HashMap.this.removeEntryForKey(o) != null; } public void clear() { HashMap.this.clear(); } } public Collection<V> values() { Collection<V> vs = values; return (vs != null ? vs : (values = new Values())); } private final class Values extends AbstractCollection<V> { public Iterator<V> iterator() { return newValueIterator(); } public int size() { return size; } public boolean contains(Object o) { return containsValue(o); } public void clear() { HashMap.this.clear(); } } public Set<Map.Entry<K,V>> entrySet() { return entrySet0(); } private Set<Map.Entry<K,V>> entrySet0() { Set<Map.Entry<K,V>> es = entrySet; return es != null ? es : (entrySet = new 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(); } } private void writeObject(java.io.ObjectOutputStream s) throws IOException { Iterator<Map.Entry<K,V>> i = (size > 0) ? entrySet0().iterator() : null; // Write out the threshold, loadfactor, and any hidden stuff s.defaultWriteObject(); // Write out number of buckets s.writeInt(table.length); // Write out size (number of Mappings) s.writeInt(size); // Write out keys and values (alternating) if (i != null) { while (i.hasNext()) { Map.Entry<K,V> e = i.next(); s.writeObject(e.getKey()); s.writeObject(e.getValue()); } } } private static final long serialVersionUID = 362498820763181265L; private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { // Read in the threshold, loadfactor, and any hidden stuff s.defaultReadObject(); // Read in number of buckets and allocate the bucket array; int numBuckets = s.readInt(); table = new Entry[numBuckets]; init(); // Give subclass a chance to do its thing. // Read in size (number of Mappings) int size = s.readInt(); // Read the keys and values, and put the mappings in the HashMap for (int i=0; i<size; i++) { K key = (K) s.readObject(); V value = (V) s.readObject(); putForCreate(key, value); } } // These methods are used when serializing HashSets int capacity() { return table.length; } float loadFactor() { return loadFactor; } }
問題:
(1)為什麼HashMap的容量一定要是2的n次冪? 因為我們在計算hash值的時候一般採用除留餘數法計算,例如15%4=3;我們計算機在計算的時候使用&運算子更高效的實現除留餘數法,也就是h%length=h&(length-1),當陣列長度是2的n次冪時保證了length-1的最後一位為1,從而保證了取索引操作的h&(length-1)的最後一位同時為0和1的可能性,保證了雜湊的均勻性。當length為奇數時,length-1的最後一位為0,這樣&操作的最後一位一定是0,那麼索引位置就一定是偶數,導致陣列的奇數位置全部沒有放置元素,浪費空間。
HashMap原始碼(JDK8)
這裡並沒有將全部原始碼都分析,原因是JDK8中新增好多新的方法,這些方面沒有使用過,所以也沒過於關心,同時這裡只羅列出了一下比較重要的方法。
類的屬性
//初始容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//預設載入因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//當桶(bucket)上的節點數大於這個值是會轉換為紅黑樹
static final int TREEIFY_THRESHOLD = 8;
//當桶(bucket)上的節點數小於這個值會轉換為連結串列
static final int UNTREEIFY_THRESHOLD = 6;
//桶中結構轉換為紅黑樹對應的table最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
//儲存元素的陣列,一定是2的n次冪
transient Node<K,V>[] table;
//存放具體的元素的集合
transient Set<Map.Entry<K,V>> entrySet;
// 存放元素的個數,注意這個不等於陣列的長度
transient int size;
//改變計數
transient int modCount;
//臨界值,當實際大小(容量*載入因子)超過臨界值,進行擴容
int threshold;
//載入因子
final float loadFactor;
建構函式
//指定容量值和載入因子的建構函式,容量值<0丟擲異常;容量值大於1 << 30,設定容量值為該值
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//載入因子小於0丟擲異常
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
//指定容量值,使用預設載入因子建構函式
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//預設建構函式,沒有指定容量值則為Null
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//將集合中的元素新增到hashMap中
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
針對建構函式中呼叫tableSizeFor,如下:
//返回大於cap的最小二次冪值
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
分析一下這個演算法:
首先是cap-1,這是為了防止,cap已經是2的冪。如果cap已經是2的冪, 又沒有執行這個減1操作,則執行完後面的幾條無符號右移操作之後,返回的capacity將是這個cap的2倍
第一次右移: n |= n >>> 1;因為n不等於0,那麼在二進位制中總會有一個bit是1,這時考慮最高位的1,通過無符號右移一位,那麼最高位的1右移1位,再做或操作,使得n中的二進位制中的與最高位的1緊鄰的右邊一位也是1.
第二次右移n |= n >>> 2;同理最高位的1右移了2位,然後與原值或,這樣二進位制中的最高位中會有4個連續的1。
第三次右移:n |= n >>> 4;同理最高位的1右移了4為,然後與原值或,這樣二進位制中的最高位會有8個連續的1.
以此類推。。。。
經過上面的我們看到,容量最大就是32位都為1,已經大於了我們的最大值,所以取最大值。經過上面的推算,我們可以看到,給定一個數,經過上面的右移操作,最終會對這個資料沒有什麼操作
針對於建構函式中呼叫putMapEntries()說明:
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
//判斷table是否已經初始化,沒有初始化,s就是實際元素
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 計算得到的t大於閾值,則初始化閾值
if (t > threshold)
threshold = tableSizeFor(t);
}
// 已初始化,並且m元素個數大於閾值,進行擴容處理
else if (s > threshold)
resize();
// 將m中的所有元素新增至HashMap中
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
重要函式
(1)putVal函式,同時hashmap中有一個put函式,也是呼叫了putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//table是否為空,為空或者長度=0,進行擴容;table不為空,根據計算出來的hash插入對應的陣列索引
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//根據計算出的hash確定元素放到哪個桶中,桶為空,新生成節點放入桶中
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//桶中已經存在元素
Node<K,V> e; K k;
//比較桶中的元素的key是否存在,存在直接覆蓋value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//不存在,先判斷是不是紅黑樹節點,是紅黑樹直接放到樹中,不是紅黑樹,放到連結串列中
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//在連結串列最末端插入節點
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//判斷結束數量是否達到8,轉換為紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 判斷連結串列中結點的key值與插入的元素的key值是否相等,
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 表示在桶中找到key值、hash值與插入元素相等的結點,覆蓋舊值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
讓我們梳理一下putVal()的邏輯:
(2)resize()函式
final Node<K,V>[] resize() {
//儲存當前table
Node<K,V>[] oldTab = table;
//table大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//儲存當前閥值
int oldThr = threshold;
int newCap, newThr = 0;
//之前table大於0
if (oldCap > 0) {
//之前table大於最大值,閥值為最大整形,並返回
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//之前table沒有達到最大值,容量翻倍,左移效率高
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//之前閥值大於0,新容量=原閥值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else {
//新容量為預設16,新閥值為16*0.75
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//新的閥值==0
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//之前table已經初始化過
if (oldTab != null) {
//遍歷原table
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;//e指向oldTab陣列中的元素,即每個連結串列中的頭結點
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)//連結串列中只有一個頭結點
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)//判斷是否是紅黑樹節點,是的話,呼叫split調整
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//是連結串列,對連結串列進行秩序維護,因為我們使用了2倍擴容,所以桶中的元素必須是要麼待在原來索引的對應位置,要麼在新的桶中的位置便宜2倍
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 將同一桶中的元素根據(e.hash & oldCap)是否為0進行分割,分成兩個不同的連結串列,完成rehash
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
梳理一下,做了什麼優化:
我們在擴容的時候使用的是2的次冪的擴充套件,所以元素的位置要麼在原位置要麼在原位置再一定2次冪的位置,如圖,原容量為16,那麼n-1 = 0000 1111,擴大2次冪就是 0001 1111.圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例,圖(b)表示擴容後key1和key2兩種key確定索引位置的示例,其中hash1是key1對應的雜湊與高位運算結果.
元素在重新計算hash之後,因為n變為2倍,那麼n-1的mask範圍在高位多1bit(紅色),因此新的index就會發生這樣的變化。
因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”,可以看看下圖為16擴充為32的resize示意圖:
(3)getNode函式,HashMap中的get呼叫給使用者的,就是通過呼叫getNode函式
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//先判斷是否是紅黑樹,如果是紅黑樹,直接從紅黑樹中拿取,如果不是從連結串列中獲取資料
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
(4)removeNode()函式
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//table陣列非空,鍵的hash值所指向的陣列中的元素非空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;//node指向最終的結果結點,e為連結串列中的遍歷指標
if (p.hash == hash && //檢查第一個節點,如果匹配成功
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
//如果第一個節點匹配不成功,則向後遍歷連結串列查詢
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
HashMap遍歷
1.entrySet獲取鍵值對,再通過Iterator遍歷
Iterator iterator = hashMap.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry entity = ( Map.Entry) iterator.next();
System.out.println("key="+entity.getKey()+",value="+entity.getValue());
}
2.通過keySet獲取鍵的set集合,再通過Iterator遍歷鍵集合
Iterator iterator1 = hashMap.keySet().iterator();
while (iterator1.hasNext()){
String key = iterator1.next().toString();
System.out.println("key="+key+",value="+hashMap.get(key));
}
HashMap問題:
(1)說明一下tableSizeFor是如何保證拿到大於等於容量的最小的2的次冪的? 首先我們將cap-1,然後得到的值在右移一位,這樣就保證最高位的1右移1位,同時跟原來的值或操作,那麼就保證最高位旁邊有兩個連續的1。同理右移2位,就有4個1,右移4為8個一,這樣最大的值就是2的32次冪。如果我們傳入任意一個數,在操作的過程中,會右移然後或操作,導致數不變,那麼此時就會得到大於等於容量的最小的2次冪
(2)描述一下java8中HashMap是如何進行put?
我們在put值的時候,會呼叫putVal函式,先判斷table是否為空(table的長度是否為0),如果為空,先進行擴容;如果不為空根據鍵Key計算hash值,得到陣列索引。判斷table[i]是否為空。為空直接插入;不為空先判斷key是否存在,存在直接覆蓋;不存在先判斷該節點是不是紅黑樹,是紅黑樹直接插入鍵值對;不是紅黑樹開始遍歷連結串列插入資料,判斷連結串列長度是否大於8,是的話轉換為紅黑樹,不是的連結串列插入,若Key存在覆蓋。最後判斷容量值是否需要擴容。 (3)描述一下java8中的resize()的實現方式?巧妙之處在於哪裡?
jdk7中在擴容的時候使用的是hash值與陣列長度-1這個掩碼進行與運算,得到新的Entry元素的新下標位置.
jdk8中擴容的時候使用的hash值與陣列長度進行與運算,因為陣列長度是2次冪的長度,所以得到的是0或者非0,0表示新位置不變,是1索引變成“原索引+原容量”
相關推薦
Java集合(四)HashMap詳解
HashMap簡介 java.lang.Object ↳ java.util.AbstractMap<K, V> ↳ java.util.HashMap<K, V> public class HashMap<K,V> ex
【第17天】Java集合(四)---Sorted介面實現的TreeSet集合及單值型別集合總結
1 TreeSet簡介 2 基本用法與特點 3 制定單值比較規則 3.1 自然排序(compareTo(Object obj)) 3.2 定製排序(定義比較器類) 3.2.1 普通類內定義
java集合(5)—hashmap
越努力越幸運! hashmap HashMap:它根據鍵的hashCode值儲存資料,大多數情況下可以直接定位到它的值,因而具有很快的訪問速度,但遍歷順序卻是不確定的。 HashMap最多隻允許一條記錄的鍵為null,允許多條記錄的值為null。HashMap非執行
Java核心(一)Thread詳解
一、概述 在開始學習Thread之前,我們先來了解一下 執行緒和程序之間的關係: 執行緒(Thread)是程序的一個實體,是CPU排程和分派的基本單位。 執行緒不能夠獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制。 執行緒和程序的關係是:執行緒是屬於
【C#MVC】搭建.netMVC4工程(四)錯誤詳解-ErrorGeneratingOutput
遇到一個問題,不知道各位有沒有遇到過。在網上搜索沒有中文的解決方案。 問題描述: 解決辦法:在你的系統變數中找到名為VS120COMNTOOLS的變數: 去你們本地的資料夾(每個人根據安裝目錄不一致,可能不太一樣),你會發現在指向的目錄下沒有錯
Java NIO(一)FileChannel 詳解
@個人筆記整理,個人理解整理,請勿輕信!請勿輕信! 目錄 系列目錄 FileChannel概述 FileChannel是面向檔案的通道,可以從FileInputStream、FileOutputStream和RandomAccess
Java集合(四):TreeMap原始碼解析
前言今天來介紹下TreeMap,TreeMap是基於紅黑樹結構實現的一種Map,要分析TreeMap的實現首先就要對紅黑樹有所瞭解。構造圖如下:藍色線條:繼承綠色線條:介面實現正文TreeMap底層是基於紅黑樹(Red-Black tree)實現,所以在學習TreeMap之前
caddy(四)Run詳解
caddy(四)Run詳解 前言 平時我們使用 caddy 都是使用 它的 二進位制 分發檔案,我們現在來分析 caddy 的 Run 函式。從最外層抽象的看它都做了些什麼。 Caddy Run 我們來看看 Caddy Run 中引入了哪些包和操作,對 Caddy 的總體行為做一個概覽caddy/cadd
Kubernetes筆記(四):詳解Namespace與資源限制ResourceQuota,LimitRange
前面我們對K8s的基本元件與概念有了個大致的印象,並且基於K8s實現了一個初步的CI/CD流程,但對裡面涉及的各個物件(如Namespace, Pod, Deployment, Service, Ingress, PVC等)及各物件的管理可能還缺乏深入的理解與實踐,接下來的文章就讓我們一起深入K8s的各元件內
Java集合源碼分析(四)HashMap
cto 情況下 base 分布 我們 ron 建立 city 不同 一、HashMap簡介 1.1、HashMap概述 HashMap是基於哈希表的Map接口實現的,它存儲的是內容是鍵值對<key,value>映射。此類不保證映射的順序,假定哈希函數將元
Java垃圾回收(GC)機制詳解
nbsp 引用計數 維護 png 對象 最新 新的 com 前沿 垃圾回收算法有兩種,根據不同的虛擬機策略不同 1、引用計數法 2、可達性分析法 由於我們平常使用的hotspot虛擬機用的是第二種。 那哪些是可達的呢? 這個算法的基本思想是通過一系列稱為“GC Roots”
Java並發編程(一)Thread詳解
能夠 lds readn 暫停 正在執行 思考 基本 進程 -c 一、概述 在開始學習Thread之前,我們先來了解一下 線程和進程之間的關系: 線程(Thread)是進程的一個實體,是CPU調度和分派的基本單位。 線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供
Java核心(四)你不知道的數據集合
狀態改變 strong 復雜度 super cit null 核心技術 就是 返回 導讀:Map竟然不屬於Java集合框架的子集?隊列也和List一樣屬於集合的三大子集之一?更有隊列的正確使用姿勢,一起來看吧! Java中的集合通常指的是Collection下的三個集合框
Java虛擬機器 :Java垃圾回收(GC)機制詳解
轉自:http://www.importnew.com/28413.html 哪些記憶體需要回收? 哪些記憶體需要回收是垃圾回收機制第一個要考慮的問題,所謂“要回收的垃圾”無非就是那些不可能再被任何途徑使用的物件。那麼如何找到這些物件? 1、引用計數法 這個演算法的實現是,給物件中新
【第18天】Java集合(五)---Map介面概述及Map介面實現的HashMap類、SortedMap介面實現的TreeMap類
1 Map的通性 1.1 基本用法與特點 1.2 遍歷 2 HashMap集合的特性 3 TreeMap集合的特性 1 Map的通性 &nb
Spring Cloud系列(二十四) 路由詳解(Finchley.RC2版本)
傳統路由配置 傳統路由配置就是不需要依賴服務發現機制,通過在配置檔案中具體指定每個路由表示式與服務例項的對映關係來實現API閘道器對外請求的路由。 單例項配置 通過zuul.routes.<route>.path與zuul.routes.<route&
Java單元測試工具:JUnit4(三)——JUnit詳解之執行流程及常用註解
(三)執行流程及常用註解 這篇筆記記錄JUnit測試類執行時,類中方法的執行順序;以及JUnit中常用的註解。 1.JUnit的執行流程 1.1 新建測試類
Java併發程式設計(一)Thread詳解
一、概述 在開始學習Thread之前,我們先來了解一下 執行緒和程序之間的關係: 執行緒(Thread)是程序的一個實體,是CPU排程和分派的基本單位。 執行緒不能夠獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制。 執行緒和程序的關係是:執行
Java容器(四):HashMap(Java 7)的實現原理
一、HashMap的定義和建構函式 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, S
Java NIO學習(二)SelectionKey詳解
書接上文 上一篇部落格中的結尾講到將Channel註冊在某個Selector的管轄範圍之下: channel.register(selector, SelectionKey.OP_ACCEPT); 由這個register()方法的第二個引數Select