文章目錄
Java 基礎 ——HashMap構造、PUT、GET
- HashMap的數據結構包括了初始數組、鏈表、紅黑樹;
- 插入數據的時候使用key%size來進行插入數據;
- 當兩個或者兩個以上的key的key相同,且key值不同的時候(即%【取餘】發生衝突,就會掛在數組初始化位置的鏈表後)
- 當某個節點後出現過多的鏈表節點的時候,就會換成紅黑樹以提高效率;
HashMap結構
Key是通過Set組織的,即不允許重複
/**
* Returns a {@link Set} view of the keys contained in this map.
* The set is backed by the map, so changes to the map are
* reflected in the set, and vice-versa. If the map is modified
* while an iteration over the set is in progress (except through
* the iterator's own <tt>remove</tt> operation), the results of
* the iteration are undefined. The set supports element removal,
* which removes the corresponding mapping from the map, via the
* <tt>Iterator.remove</tt>, <tt>Set.remove</tt>,
* <tt>removeAll</tt>, <tt>retainAll</tt>, and <tt>clear</tt>
* operations. It does not support the <tt>add</tt> or <tt>addAll</tt>
* operations.
*
* @return a set view of the keys contained in this map
*/
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
Value是通過Collection組織的,即體現允許重複數據(ArrayList、LinkedList)
/**
* Returns a {@link Collection} view of the values contained in this map.
* The collection is backed by the map, so changes to the map are
* reflected in the collection, and vice-versa. If the map is
* modified while an iteration over the collection is in progress
* (except through the iterator's own <tt>remove</tt> operation),
* the results of the iteration are undefined. The collection
* supports element removal, which removes the corresponding
* mapping from the map, via the <tt>Iterator.remove</tt>,
* <tt>Collection.remove</tt>, <tt>removeAll</tt>,
* <tt>retainAll</tt> and <tt>clear</tt> operations. It does not
* support the <tt>add</tt> or <tt>addAll</tt> operations.
*
* @return a view of the values contained in this map
*/
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new Values();
values = vs;
}
return vs;
}
HashMap(JDK8之前)
HashMap是數組+鏈表存儲結構
- 數組特點是查詢速度快、增刪較慢
- 鏈表特點是查詢速度慢、增刪較快
HashMap結合兩者特點組成
如圖所示(HashMap存儲結構 JDK8之前):
HashMap數組長度默認是16,16個數組(桶)中每個元素存儲的就是鏈表的頭結點
/**
* 默認初始容量,必須是2的冪次方,即桶默認是16個
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
HashMap(JDK8之後)
HashMap是數組+鏈表+紅黑樹結構
- 數組特點是查詢速度快、增刪較慢
- 鏈表特點是查詢速度慢、增刪較快
- 通過常量TREEIFY_THRESHOLD來控制是否將鏈表轉換成紅黑樹來存儲
以下對HashMap基於JDK8講解
HashMap內部結構
通過數組Node<K,V>與鏈表Set<Map.Entry<K,V>>組成的複合結構
- 基礎結構
// 數組結構
transient Node<K,V>[] table;
// 鏈表結構
transient Set<Map.Entry<K,V>> entrySet;
- Node<K,V>
/**
* Node是通過hash值、鍵值對、以及指向下一個節點來組成的
*/
static class Node<K,V> implements Map.Entry<K,V> {
// hash值
final int hash;
// 鍵
final K key;
// 值
V value;
// 指向下一個節點
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
數組被分爲一個個的backet(桶),通過hash值決定了鍵值對在這個數組的尋址,相同的鍵值對以鏈表形式存儲!!!
鏈表的大小超過TREEIFY_THRESHOLD閾值(默認是8),會改造成紅黑樹
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
// TREEIFY_THRESHOLD(樹化)
static final int TREEIFY_THRESHOLD = 8;
而某個桶,上面的鏈表由於刪除了某些值,低於了閾值(UNTREEIFY_THRESHOLD),紅黑樹又被轉變爲鏈表,來確保性能
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6;
HashMap構造函數
構造函數不是直接指定大小,而是給一些成員變量賦值,所以HashMap是在首次使用的時候才被初始化。
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and load factor.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @throws IllegalArgumentException if the initial capacity is negative
* or the load factor is nonpositive
*/
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);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
/**
* Constructs an empty <tt>HashMap</tt> with the specified initial
* capacity and the default load factor (0.75).
*
* @param initialCapacity the initial capacity.
* @throws IllegalArgumentException if the initial capacity is negative.
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* Constructs an empty <tt>HashMap</tt> with the default initial capacity
* (16) and the default load factor (0.75).
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* Constructs a new <tt>HashMap</tt> with the same mappings as the
* specified <tt>Map</tt>. The <tt>HashMap</tt> is created with
* default load factor (0.75) and an initial capacity sufficient to
* hold the mappings in the specified <tt>Map</tt>.
*
* @param m the map whose mappings are to be placed in this map
* @throws NullPointerException if the specified map is null
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
總結HashMap結構
HashMap是數組+鏈表+紅黑樹的結構
默認數組爲16個桶
構造函數不做大小指定,在首次使用(構造函數指定大小或者put)時對大小進行指定
具體使用數組、鏈表、紅黑樹如何轉變的通過put函數說明。
HashMap添加元素(put)
- 源碼分析
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
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 爲空時,調用resize()方法來初始化table
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 進行hash運算,算出鍵值對,在table中的具體位置
if ((p = tab[i = (n - 1) & hash]) == null)
// 當沒有元素時,直接new該鍵值對的node放到數組當中
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果存在鍵值對,且傳入進來的鍵值對是一致的,則直接替換數組中的元素
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);
// 如果不是樹化的,按照鏈表的插入方式往鏈表中添加元素,同時判斷鏈表元素的總數,一旦超過TREEIFY_THRESHOLD,則將鏈表進行樹化
else {
for (int binCount = 0; ; ++binCount) {
// 如果不是樹化的,按照鏈表的插入方式往鏈表中添加元素
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 同時判斷鏈表元素的總數(binCount),一旦超過TREEIFY_THRESHOLD,則將鏈表進行樹化(treeifyBin)
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 如果插入的鍵位存在於HashMap中,則對對應的鍵位進行更新操作
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 當HashMap中的size,當元素大於閾值(threshold)時,對hashMap進行擴容(resize方法)
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
resize即具備初始化,也具備擴容的功能
總結put邏輯
1.若HashMap未被初始化,則進行初始化操作;
2.對Key求Hash值,依據Hash值計算下標;
3.若未發生碰撞,則直接放入桶中;
"碰撞":就是計算得到相同的hash值4.若發生碰撞,則以鏈表的方式鏈接到後面;
5.若鏈表長度超過閾值,且HashMap元素超過最低樹化容量,則將鏈表轉成紅黑樹;
默認閾值:TREEIFY_THRESHOLD=8
默認最低樹化容量:MIN_TREEIFY_CAPACITY=64
即:當前桶的容量超過8,並且整個HashMap的元素超過64就會將鏈表轉換爲紅黑樹。如果桶容量超過8,但是整個HashMap元素沒有超過64,只會發生resize(擴容),而不會發生樹化(紅黑樹樹化)
6.若節點已存在,則用新值替換舊值;
7.若桶滿了(默認容量16*擴容因子0.75),就需要resize(擴容2倍後重排);
HashMap獲取元素(get)
主要使用鍵值對的hashcode,通過hash算法找到backet(桶)的位置,找到backet位置調用key.equlas(k)方法,去找到鏈表中正確的節點,最終找到要找的值並返回
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 通過hash算法找到backet(桶)的位置 (first = tab[(n - 1) & hash]) != null)
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node 找到backet位置調用key.equlas(k)方法,去找到鏈表中正確的節點
((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;
}
總結
- HashMap由數組+鏈表+紅黑樹組成
- 數組也被稱爲桶(backed),默認爲16個桶
- 在PUT操作時,會先進行hash運算,計算數組下標是否出現“hash碰撞”
- 如果沒有出現hash碰撞,則直接插入桶中
- 如果出現hash碰撞,則以鏈表方式鏈接到這個桶的後邊(默認每個桶大小爲8個)
- 當桶中鏈表長度超過閾值(8)時,且hashMap桶超過(64)個時,則發生樹化(紅黑樹樹化)
- 若節點已經存在,則用新值替換舊值
- 若桶滿了(默認容量16*擴容因子0.75),就需要resize(擴容2倍後重排)