java集合(4):HashMap原始碼分析(jdk1.8)
前言
- Map介面雖然也是集合體系中的重要一個分支,但是Map介面並不繼承自Collection,而是自成一派。
public interface Map<K,V>
Map集合儲存鍵對映到值的物件。一個集合中不能包含重複的鍵,每個鍵最多隻能對映到一個值。
Map 介面提供三種collection 檢視(意思是3種迭代器),允許以鍵集、值集或鍵-值對映關係集的形式來迭代某個Map的內容。對映順序 定義為迭代器在對映的 collection 檢視上返回其元素的順序。某些對映實現可明確保證其順序,如 TreeMap 類;另一些對映實現則不保證順序,如 HashMap 類。
Map介面最常用的兩個實現類就是HashMap和TreeMap。前者以雜湊表結構儲存元素,後者以二叉樹結構儲存元素,而且Set介面下的HashSet底層結構就是HashMap,所以講完HashMap再講HashSet就很簡單了。
正文
一,雜湊表結構
- 雜湊表結構是資料結構中的重要概念之一,其原理不再贅述,本文重點介紹雜湊表結構的程式碼體現。需要說明的一點是:jdk1.8以前的HashMap只使用了陣列和連結串列的形式,這樣可能導致陣列某個位置上的連結串列過長,不利於查詢。jdk1.8以後,如果某位置的連結串列超過一定長度就會轉為紅黑樹,這樣就有利於查詢。
二,HashMap原始碼
1, 類名及常量
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
// Map集合初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// Map集合最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 預設載入因子,用於動態擴充陣列容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 當連結串列上的節點數大於等於8時,連結串列轉為樹結構
static final int TREEIFY_THRESHOLD = 8;
// 當連結串列上的節點小於6時,樹結構轉連結串列
static final int UNTREEIFY_THRESHOLD = 6;
//
static final int MIN_TREEIFY_CAPACITY = 64;
}
2,成員變數
// 儲存元素的陣列,每個元素都是Node型的,Node是一個靜態內部類
transient Node<K,V>[] table;
// entrySet()方法返回的結果集
transient Set<Map.Entry<K,V>> entrySet;
// Map元素個數
transient int size;
// Map物件被修改的次數
transient int modCount;
// 增加陣列容量的閾值
int threshold;
// 載入因子
final float loadFactor;
3,Node型別元素的內部類形式
static class Node<K,V> implements Map.Entry<K,V> { // 實現Map介面的內部介面Entry
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;
}
}
4,HashMap的4個構造方法
public HashMap() { // 1,無參構造方法,成員變數使用預設值
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
public HashMap(int initialCapacity) { // 2,初始容量的構造方法
this(initialCapacity, DEFAULT_LOAD_FACTOR); // 去呼叫2個引數的構造方法
}
public HashMap(int initialCapacity, float loadFactor) { // 3,指定容量和載入因子
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;
// 呼叫計算容量的方法,返回大於initialCapacity的最小2的冪。
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(Map<? extends K, ? extends V> m) { // 4,接收一個map物件
this.loadFactor = DEFAULT_LOAD_FACTOR; // 預設載入因子
putMapEntries(m, false); // 新增m到結構
}
5,put()方法詳解,該方法也是最重要的方法,下面是方法的執行流程圖和原始碼分析
執行過程:
①.判斷鍵值對陣列table[i]是否為空或為null,否則執行resize()進行擴容;
②.根據鍵值key計算hash值得到插入的陣列索引i,如果table[i]==null,直接新建節點新增,轉向⑥,如果table[i]不為空,轉向③;
③.判斷table[i]的首個元素是否和key一樣,如果相同直接覆蓋value,否則轉向④,這裡的相同指的是hashCode以及equals;
④.判斷table[i] 是否為treeNode,即table[i] 是否是紅黑樹,如果是紅黑樹,則直接在樹中插入鍵值對,否則轉向⑤;
⑤.遍歷table[i],判斷連結串列長度是否大於8,大於8的話把連結串列轉換為紅黑樹,在紅黑樹中執行插入操作,否則進行連結串列的插入操作;遍歷過程中若發現key已經存在直接覆蓋value即可;
⑥.插入成功後,判斷實際存在的鍵值對數量size是否超多了最大容量threshold,如果超過,進行擴容。
put方法原始碼:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true); // 呼叫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,則進行初始化
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;
// 如果節點hash值相同且key相同,則直接覆蓋
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存在,直接覆蓋
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;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount; // map結構被修改次數自增
if (++size > threshold) // 新增元素後,如果陣列長度超過閾值,則擴容。
resize();
afterNodeInsertion(evict);
return null;
}
5.1,擴容機制
擴容(resize)就是重新計算容量,向HashMap物件裡不停的新增元素,而HashMap物件內部的陣列無法裝載更多的元素時,物件就需要擴大陣列的長度,以便能裝入更多的元素。當然Java裡的陣列是無法自動擴容的,方法是使用一個新的陣列代替已有的容量小的陣列,就像我們用一個小桶裝水,如果想裝更多的水,就得換大水桶。
陣列的容量是2的次冪,也就是說每次擴充套件,長度擴充套件為原來的2倍。接著元素也要變換位置,那麼,元素的位置要麼是在原位置,要麼是在原位置加上2的次冪的位置。所以,我們在擴充的時候,不需要再重新計算hash值,而是看原先的hash值新增的1bit是1還是0。如果是0,索引不變,元素待在原位;如果是1,新索引為“原索引+擴充套件長度”。這個設計確實非常的巧妙,既省去了重新計算hash值的時間,而且同時,由於新增的1bit是0還是1可以認為是隨機的,因此resize的過程,均勻的把之前的衝突的節點分散到新的bucket了。這一塊就是JDK1.8新增的優化點。
- 擴容方法resize()
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 當前陣列賦值給oldTab
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold; // 當前閾值賦值給oldThr
int newCap, newThr = 0; // 新陣列容量,新閾值
if (oldCap > 0) {
// 如果超過最大容量,就不再擴容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果老容量擴大2倍仍不超過最大值,則新容量為原來的2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 計算新的閾值
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;
// 將每個元素移動到新的陣列上
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e; // 迴圈取出老陣列上的每一個元素
if ((e = oldTab[j]) != null) {
oldTab[j] = null; // 將老陣列元素置空,讓垃圾回收器回收
// 如果陣列元素沒有連結串列,直接新增到新陣列
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果e是樹節點,則按照樹結構處理該分支
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// 如果e是連結串列節點,則按照連結串列結構處理該分支
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 原索引
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;
}
6,get()方法詳解,get的過程就是遍歷的過程。
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; // tab是table的副本
Node<K,V> first, e; // first代表陣列上鍊表的第一個元素
int n;
K k; // k是first元素的key屬性副本
// 如果陣列不為空,陣列長度大於0,陣列上鍊表第一個元素不為空,則進行遍歷
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 如果引數的hash值和key與first的hash值和key相同,則返回first
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;
}
7,keySet()和entrySet()方法,
- Map本身沒有迭代器,若要迭代,需要將Map轉化為Set集合,使用Set集合的迭代器進行迭代。如下圖,這兩個方法均返回一個Set集合物件,分別將key和(key,value)作為整體構成一個Set。
方法原始碼:
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet(); // KetSet是個內部類,繼承自Set體系,所以有迭代器方法
keySet = ks;
}
return ks;
}
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
// EntrySet也是內部類,同樣繼承自Set體系,有增強的迭代器方法
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
這兩個方法常見用法:
package com.jimmy.map;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class MapDemo1 {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
System.out.println(map.put("name1", "jimmy"));
System.out.println(map.put("name2", "jimmy"));
System.out.println(map.put("name3", "jimmy"));
Set<String> keySet = map.keySet(); // keySet()方法返回map集合中所有key的set集合
for (String eachKey : keySet) {
System.out.println(eachKey+"::"+map.get(eachKey));
}
System.out.println("------------------");
// entrySet()方法返回map集合中(k,v)對的set集合
Set<Entry<String, String>> entrySet = map.entrySet();
for (Entry<String, String> eachMap : entrySet) {
System.out.println(eachMap.getKey()+"--"+eachMap.getValue());
}
}
}
總結
map結構相對複雜,其實現類按照不同的資料結構(雜湊表結構或者樹結構)來儲存元素,應該在學習了相應資料結構的基礎上再學習相應的集合實現。