前面已經介紹完了Collection接口下的集合實現類,今天我們來介紹Map接口下的兩個重要的集合實現類HashMap,TreeMap。關於Map的一些通用介紹,可以參考第一篇文章。由於Map與List、Set集合的某些特性有重合,因此觀看本篇文章的會參考到之前的一些內容,最下方有鏈接。如果已經有這方面的基礎,那麼對Map的學習將會事半功倍。
HashMap
HashMap 是一個散列表,它存儲的內容是鍵值對(key-value)映射。
既然要介紹HashMap,那麼就順帶介紹HashTable,兩者進行比對。HashMap和Hashtable都是Map接口的經典實現類,它們之間的關係完全類似於之前介紹的ArrayList和Vector的關係。由於Hashtable是個古老的Map實現類(從Hashtable的命名規範就可以看出,t沒有大寫,並不是我寫錯了),需要方法比較繁瑣,不符合Map接口的規範。但是Hashtable也具有HashMap不具有的優點。下面我們進行兩者之間的比對。
HashMap與Hashtable的區別
1.Hashtable是一個線程安全的Map實現,但HashMap是線程不安全的實現,所以HashMap比Hashtable的性能好一些;但如果有多個線程訪問同一個Map對象時,是盜用Hashtable實現類會更好。
2.Hashtable不允許使用null作爲key和value,如果試圖把null值放進Hashtable中,將會引發NullPointerException異常;但是HashMap可以使用null作爲key或value。
HashMap判斷key與value相等的標準
前面文章中,我們針對其他集合都分析了判斷集合元素相等的標準。針對HashMap也不例外,不同的是有兩個元素:key與value需要分別介紹判斷相等的標準。
key判斷相等的標準
類似於HashSet,HashMap與Hashtable判斷兩個key相等的標準是:兩個key通過equals()方法比較返回true,兩個key的hashCode值也相等,則認爲兩個key是相等的。
注意:用作key的對象必須實現了hashCode()方法和equals()方法。並且最好兩者返回的結果一致,即如果equals()返回true,hashCode()值相等。可參考Set關於這方面的介紹。
value判斷相等的標準
HashMap與Hashtable判斷兩個value相等的標準是:只要兩個對象通過equals()方法比較返回true即可。
注意: HashMap中key所組成的集合元素不能重複,value所組成的集合元素可以重複。
下面程序示範了HashMap判斷key與value相等的標準。
public class A {
public int count;
public A(int count) {
this.count = count;
}
//根據count值來計算hashCode值
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + count;
return result;
}
//根據count值來判斷兩個對象是否相等
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
A other = (A) obj;
if (count != other.count)
return false;
return true;
}
}
public class B {
public int count;
public B(int count) {
this.count = count;
}
//根據count值來判斷兩個對象是否相等
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
B other = (B) obj;
if (count != other.count)
return false;
return true;
}
}
public class HashMapTest {
public static void main(String[] args){
HashMap map = new HashMap();
map.put(new A(1000), "集合Set");
map.put(new A(2000), "集合List");
map.put(new A(3000), new B(1000));
//僅僅equals()比較爲true,但認爲是相同的value
boolean isContainValue = map.containsValue(new B(1000));
System.out.println(isContainValue);
//雖然是不同的對象,但是equals()和hashCode()返回結果都相等
boolean isContainKey = map.containsKey(new A(1000));
System.out.println(isContainKey);
//equals()和hashCode()返回結果不滿足key相等的條件
System.out.println(map.containsKey(new A(4000)));
}
}
輸出結果:
true
true
false
注意:如果是加入HashMap的key是個可變對象,在加入到集合後又修改key的成員變量的值,可能導致hashCode()值以及equal()的比較結果發生變化,無法訪問到該key。一般情況下不要修改。
HashMap的本質
下面我們從源碼角度來理解HashMap。
HashMap的構造函數
// 默認構造函數。
HashMap()
// 指定“容量大小”的構造函數
HashMap(int capacity)
// 指定“容量大小”和“加載因子”的構造函數
HashMap(int capacity, float loadFactor)
// 包含“子Map”的構造函數
HashMap(Map<? extends K, ? extends V> map)
從構造函數中,瞭解到兩個重要的元素:容量大小(capacity)以及加載因子(loadFactor)。
容量(capacity)是哈希表的容量,初始容量是哈希表在創建時的容量(即DEFAULT_INITIAL_CAPACITY = 1 << 4
)。
加載因子 是哈希表在其容量自動增加之前可以達到多滿的一種尺度。當哈希表中的條目數超出了加載因子與當前容量的乘積時,則要對該哈希表進行 resize操作(即重建內部數據結構),從而哈希表將具有大約兩倍的桶數。
通常,默認加載因子是 0.75(即DEFAULT_LOAD_FACTOR = 0.75f
), 這是在時間和空間成本上尋求一種折衷。加載因子過高雖然減少了空間開銷,但同時也增加了查詢成本(在大多數 HashMap 類的操作中,包括 get 和 put 操作,都反映了這一點)。在設置容量時應該考慮到映射中所需的條目數及其加載因子,以便最大限度地減少 resize操作次數。如果容量大於最大條目數除以加載因子,則不會發生 rehash 操作。
Node類型
HashMap是通過”拉鍊法”實現的哈希表。它包括幾個重要的成員變量:table, size, threshold, loadFactor。
table是一個Node[]數組類型,而Node實際上就是一個單向鏈表。哈希表的”key-value鍵值對”都是存儲在Node數組中的。
size是HashMap的大小,它是HashMap保存的鍵值對的數量。
threshold是HashMap的閾值,用於判斷是否需要調整HashMap的容量。threshold的值=”容量*加載因子”,當HashMap中存儲數據的數量達到threshold時,就需要將HashMap的容量加倍。
loadFactor就是加載因子。
要想理解HashMap,首先就要理解基於Node實現的“拉鍊法”。
Java中數據存儲方式最底層的兩種結構,一種是數組,另一種就是鏈表,數組的特點:連續空間,尋址迅速,但是在刪除或者添加元素的時候需要有較大幅度的移動,所以查詢速度快,增刪較慢。而鏈表正好相反,由於空間不連續,尋址困難,增刪元素只需修改指針,所以查詢速度慢、增刪快。有沒有一種數組結構來綜合一下數組和鏈表,以便發揮它們各自的優勢?答案是肯定的!就是:哈希表。哈希表具有較快(常量級)的查詢速度,及相對較快的增刪速度,所以很適合在海量數據的環境中使用。一般實現哈希表的方法採用“拉鍊法”,我們可以理解爲“鏈表的數組”,如下圖:
圖中,我們可以發現哈希表是由數組+鏈表組成的,一個長度爲16的數組中,每個元素存儲的是一個鏈表的頭結點。那麼這些元素是按照什麼樣的規則存儲到數組中呢?
一般情況是通過hash(key)獲得,也就是元素的key的哈希值。如果hash(key)值相等,則都存入該hash值所對應的鏈表中。它的內部其實是用一個Node數組來實現。
所以每個數組元素代表一個鏈表,其中的共同點就是hash(key)相等。
下面我們來了解下鏈表的基本元素Node。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
// 指向下一個節點
Node<K,V> next;
//構造函數。
// 輸入參數包括"哈希值(hash)", "鍵(key)", "值(value)", "下一節點(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;
}
// 判斷兩個Node是否相等
// 若兩個Node的“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;
}
}
再此結構下,實現了集合的增刪改查功能,由於本篇的篇幅有限,這裏就不具體介紹其源碼實現了。
HashMap遍歷方式
1.遍歷HashMap的鍵值對
第一步:根據entrySet()獲取HashMap的“鍵值對”的Set集合。
第二步:通過Iterator迭代器遍歷“第一步”得到的集合。
2.遍歷HashMap的鍵
第一步:根據keySet()獲取HashMap的“鍵”的Set集合。
第二步:通過Iterator迭代器遍歷“第一步”得到的集合。
3.遍歷HashMap的值
第一步:根據value()獲取HashMap的“值”的集合。
第二步:通過Iterator迭代器遍歷“第一步”得到的集合。
LinkedHashMap實現類
HashSet有一個LinkedHashSet子類,HashMap也有一個LinkedHashMap子類;LinkedHashMap使用雙向鏈表來維護key-value對的次序。
LinkedHashMap需要維護元素的插入順序,因此性能略低於HashMap的性能;但是因爲它以鏈表來維護內部順序,所以在迭代訪問Map裏的全部元素時有較好的性能。迭代輸出LinkedHashMap的元素時,將會按照添加key-value對的順序輸出。
本質上來講,LinkedHashMap=散列表+循環雙向鏈表
TreeMap
TreeMap是SortedMap接口的實現類。TreeMap 是一個有序的key-value集合,它是通過紅黑樹實現的,每個key-value對即作爲紅黑樹的一個節點。
TreeMap排序方式
TreeMap有兩種排序方式,和TreeSet一樣。
自然排序:TreeMap的所有key必須實現Comparable接口,而且所有的key應該是同一個類的對象,否則會拋出ClassCastException異常。
定製排序:創建TreeMap時,傳入一個Comparator對象,該對象負責對TreeMap中的所有key進行排序。
TreeMap中判斷兩個元素key、value相等的標準
類似於TreeSet中判斷兩個元素相等的標準,TreeMap中判斷兩個key相等的標準是:兩個key通過compareTo()方法返回0,TreeMap即認爲這兩個key是相等的。
TreeMap中判斷兩個value相等的標準是:兩個value通過equals()方法比較返回true。
注意:如果使用自定義類作爲TreeMap的key,且想讓TreeMap良好地工作,則重寫該類的equals()方法和compareTo()方法時應保持一致的返回結果:兩個key通過equals()方法比較返回true時,它們通過compareTo()方法比較應該返回0。如果兩個方法的返回結果不一致,TreeMap與Map接口的規則就會衝突。
除此之外,與TreeSet類似,TreeMap根據排序特性,也添加了一部分新的方法,與TreeSet中的一致。可以參考前面的文章。
TreeMap的本質
紅黑樹
R-B Tree,全稱是Red-Black Tree,又稱爲“紅黑樹”,它一種特殊的二叉查找樹。紅黑樹的每個節點上都有存儲位表示節點的顏色,可以是紅(Red)或黑(Black)。
紅黑樹的特性:
(1)每個節點或者是黑色,或者是紅色。
(2)根節點是黑色。
(3)每個葉子節點(NIL)是黑色。 [注意:這裏葉子節點,是指爲空(NIL或NULL)的葉子節點!]
(4)如果一個節點是紅色的,則它的子節點必須是黑色的。
(5)從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。
注意:
(01) 特性(3)中的葉子節點,是隻爲空(NIL或null)的節點。
(02) 特性(5),確保沒有一條路徑會比其他路徑長出倆倍。因而,紅黑樹是相對是接近平衡的二叉樹。
紅黑樹的時間複雜度爲: O(log n)
更多關於紅黑樹的增刪改查操作,可以參考這篇文章。
可以說TreeMap的增刪改查等操作都是在一顆紅黑樹的基礎上進行操作的。
TreeMap遍歷方式
遍歷TreeMap的鍵值對
第一步:根據entrySet()獲取TreeMap的“鍵值對”的Set集合。
第二步:通過Iterator迭代器遍歷“第一步”得到的集合。
遍歷TreeMap的鍵
第一步:根據keySet()獲取TreeMap的“鍵”的Set集合。
第二步:通過Iterator迭代器遍歷“第一步”得到的集合。
遍歷TreeMap的值
第一步:根據value()獲取TreeMap的“值”的集合。
第二步:通過Iterator迭代器遍歷“第一步”得到的集合。
Map實現類的性能分析及適用場景
HashMap與Hashtable實現機制幾乎一樣,但是HashMap比Hashtable性能更好些。
LinkedHashMap比HashMap慢一點,因爲它需要維護一個雙向鏈表。
TreeMap比HashMap與Hashtable慢(尤其在插入、刪除key-value時更慢),因爲TreeMap底層採用紅黑樹來管理鍵值對。
適用場景:
一般的應用場景,儘可能多考慮使用HashMap,因爲其爲快速查詢設計的。
如果需要特定的排序時,考慮使用TreeMap。
如果僅僅需要插入的順序時,考慮使用LinkedHashMap。
以上就是集合Map的內容,介紹地比較粗糙,感興趣的話可以自己看源碼深入瞭解其內部的結構。