HashMap是Java Collection Framework 的重要成員之一。HashMap是基於哈希表的 Map 接口的實現,此實現提供所有可選的映射操作,映射是以鍵值對的形式映射:key-value。key——此映射所維護的鍵的類型,value——映射值的類型,並且允許使用 null 鍵和 null 值。而且HashMap不保證映射的順序。
簡單的介紹一下HashMap,就開始HashMap的源碼分析。
首先簡單的介紹一下HashMap里都包含的數據結構。覺得還是先貼一張圖比較好,結合圖文會比較好理解一些。
現在就可以開說一下這三種數據結構。
第一個就是Node<K,V>類型的節點。
備注:static class HashMap.Node<K,V> implements Map.Entry<K,V>
第二個就是由Node<K,V>類型組成的一個Node<K,V>[] table數組。
第三個就是一個TreeNode<K,V>類型的紅黑樹。
備注:static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>
static class LinkedHashMap.Entry<K,V> extends HashMap.Node<K,V>
static class HashMap.Node<K,V> implements Map.Entry<K,V>
現在結合代碼看一看這三種數據結構(其實Node和Node[] 是同一類,所以就是兩種)。
第一種:Node<K,V>
看這個之前先看看它實現的父類接口Map.Entry<K,V>的源碼(Entry是Map接口里的一個內部接口)
interface Entry<K,V> { K getKey(); V getValue(); V setValue(V value); boolean equals(Object o); int hashCode(); }
再看Node的源碼(Node是HashMap類的一個靜態內部類,實現了Map接口里的內部接口Entry)
static class Node<K,V> implements Map.Entry<K,V> { //這四個成員變量就是一個Node節點所包含的四個變量域 //其中hash的計算方法是由一個hash方法得到的,該方法的是實現就是HashMap里,這里為了看的清楚一些,就寫到下面: // hash = (h = key.hashCode()) ^ (h >>> 16); 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; }
//拿到該Node 的key public final K getKey() { return key; }
//拿到該Node 的value public final V getValue() { return value; }
//重寫toString方法 public final String toString() { return key + "=" + value; }
//修改當前Entry對象的value為傳進入newvalue,返回值為之前的oldvalue值
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 == 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;
}
//重寫了equals,因此重新實現hashCode()
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
}
第二種:TreeNode(這里只附上TreeNode類的成員變量和一個獲取樹根的方法,其他的省略。因為TreeNode里實現了很多關於紅黑樹的操作方法,如果全部放到這里不僅看不懂而且顯得特別亂)
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { TreeNode<K,V> parent; TreeNode<K,V> left; TreeNode<K,V> right; TreeNode<K,V> prev; //構造函數 TreeNode(int hash, K key, V val, Node<K,V> next){
super(hash, key, val, next); } //返回該樹的樹根 final TreeNode<K,V> root() { for (TreeNode<K,V> r = this, p;;){ if ((p = r.parent) == null) return r; r = p;
} } //其余部分省略,有興趣的可以自行查看源代碼。 }
然后在說一下TreeNode構造函數所做的事情。
先了解一下TreeNode的繼承體系:TreeNode繼承了LinkedHashMap.Entry
LinkedHashMap.Entry繼承了HashMap.Node
HashMap.Node實現了Map.Entry
了解這個就能清楚的知道TreeNode的構造函數到底做了什么事情。底下的圖顯示了TreeNode利用構造初始化的流程。
其實最終還是調到了Node的構造,初始化TreeNode了從Node繼承而來的四個數據域(int hash,K key,V value,Node<K,V> next)。
接着我們說一下HashMap里幾個重要的成員變量和常量(省略了一部分)。
//通過HahMap實現的接口可知,其支持所有映射操作,能被克隆,支持序列化 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { private static final long serialVersionUID = 362498820763181265L; //默認Node<K,V> table的初始容量16,2的4次方。 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //table數組的最大容量為1073 741 824,2的30次方。 static final int MAXIMUM_CAPACITY = 1 << 30; /* 默認加載因子 該值和table的長度的乘積為是否對table進行擴容的標志。 例如當該值默認為0.75,table的長度為16時,就是說當table填充至 12*0.75 =12 之后,這個table就要進行擴容 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; //哈希表的定義,這個數據結構在前面已經介紹過 transient Node<K,V>[] table; /* HashMap的大小,即保存的鍵值對的數量 當該值大於等於HashMap的閾值時,數組就會擴充 */ transient int size; /* HashMap的閾值. 用於判斷是否需要調整HashMap的容量 該值的計算方法為: 加載因子 * (table.length) 當table.size>=threshold就會擴容,就是如果hashMap中存放的鍵值對大於這個閾值,就進行擴容 如果加載因子沒有被分配,默認為0.75 如果table數組沒有被分配,默認為初始容量值(16); 或若threshold值為0,也表明該值為初始容量值。 */ int threshold;
/* 加載因子 如果加載因子沒有被分配,默認為0.75 */ final float loadFactor;
//當添加到tab[i]位置下的鏈表長度達到8時將鏈表轉換為紅黑樹 static final int TREEIFY_THRESHOLD = 8; ...... }
介紹完數據結構再來說一下我們平時經常寫的代碼:Map<String,Object> map = new HashMap<String,Object>()
(這樣的方式獲得的map對象,調用的是默認無參的構造,實際上還有其他三個有參的構造函數)
要知道寫完這句代碼之后到底發生了什么。我們首先就得看一下HashMap構造器(共4個構造器),源碼如下
/*
默認無參構
構造一個具有默認初始容量 (16) 和默認加載因子 (0.75) 的空 HashMap。
*/ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; } //構造一個帶指定初始容量和加載因子的空 HashMap。 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);
//經過一些過濾和處理,現在的loadFactor和initialCapacity都為合法的值 //初始化加載因子 this.loadFactor = loadFactor;
/* 初始化HashMap的閾值 調用tableSizeFor(initialCapacity)方法 該方法根據數組的初始容量的大小求出HashMapde的閾值threshold
*/ this.threshold = tableSizeFor(initialCapacity); } /* 第三個構造方法 構造一個帶指定初始容量和默認加載因子 (0.75) 的空 HashMap。 */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } // 構造一個映射關系與指定 Map 相同的新 HashMap。 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
上面提到了根據數組初始容量得出HashMap閾值的一個方法:tableSizeFor(int 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; }
簡單的測試了一下這個方法
public class TestHashMap{
static final int MAXIMUM_CAPACITY = 1 << 30;
public static void main(String[] args){ System.out.println("---------------------"); System.out.println(" 第一次測試,cap = 0,返回的閾值threshold = "+tableSizeFor(0)); System.out.println("---------------------"); System.out.println(" 第二次測試,cap = 3,返回的閾值threshold = "+tableSizeFor(3)); System.out.println("---------------------"); System.out.println(" 第三次測試,cap = 16,返回的閾值threshold = "+tableSizeFor(16)); System.out.println("---------------------"); System.out.println(" 第四次測試,cap = 100,返回的閾值threshold = "+tableSizeFor(100)); System.out.println("---------------------"); System.out.println(" 第五次測試,cap = 1000,返回的閾值threshold = "+tableSizeFor(1000)); System.out.println("---------------------"); System.out.println(" 第六次測試,cap = 10000,返回的閾值threshold = "+tableSizeFor(10000)); System.out.println("---------------------"); } 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 = 0,返回的閾值threshold = 1
--------------------
第二次測試,cap = 3,返回的閾值threshold = 4
---------------------
第三次測試,cap = 16,返回的閾值threshold = 16
---------------------
第四次測試,cap = 100,返回的閾值threshold = 128
---------------------
第五次測試,cap = 1000,返回的閾值threshold = 1024
---------------------
第六次測試,cap = 10000,返回的閾值threshold = 16384
---------------------
總之,tableSizeFor方法是根據table數組的容量計算出hashMap可以維護出的鍵值對。從結果可以看到閾值至少是1,而且是剛好比cap大一點的2的冪。
至於為什么閾值要做成這樣,看了很多文章,都是說為了使hashMap散列均勻。但是實際上tab[i]中i的選擇是 hash & lenth-1 是和容量在做按位或。
因此對這里有一些疑惑。
到此對HashMap一部分介紹完了。哪里有不對的地方,還望指出。還有就是如果能解答我的疑惑,還請指導,十分感謝 ^_^。