概述
在現實生活中,我們要存儲某些信息的時候,比如,我們使用的微信就是一個手機號對應一個微信賬戶,這是一種成對的存儲關係。
Map 就是用來存儲“鍵(key) 值(value)”對的。通過鍵識別,所以鍵對象不能重複。
對比 Collection 中的集合,Map 集合中的元素是成對存在,每個元素由鍵與值兩部分組成,通過鍵找到所對應的值。
注意的是:Map 集合中不能包含重複的鍵,值可以重複;每個鍵對應一個值。(如果重複了鍵將會覆蓋舊的值,重複是根據 equals 方法判斷)
常用類
Map 中常用的子類爲 HashMap ,HashTable,TreeMap等。
HashMap<K,V>
存儲數據採用的哈希表結構,所以是無序的,並且爲了鍵的唯一性,不重複,需要重寫鍵的 hashCode() 方法,equals() 方法
- 概述
- 他是一個散列表,對比Collection 的單列集合,它是一個雙列集合。存儲內容是鍵值對(key-value)一對一的關係映射。 並且線程是不同步的,可以存入 null 鍵,null 值。
遍歷方式
- 遍歷 HashMap 的鍵值對
- 根據entrySet()獲取 HashMap 的“鍵值對”的 Set 的集合
- 通過 Iterator 迭代器遍歷第一步得到的集合
//Entry 相當於就是 Map 中的每個 key-value 只保存下來的單個記錄點
//每個 Entry 就保存一個 key 以及它所對應的 value 。Set是集合
Set<Entry<String, Student>> keys = map.entrySet();
Iterator<Entry<String, Student>> iterator= keys.iterator();
//當下一個有值時
while(iterator.hasNext()) {
//讀取單個的 Entry 對象
Entry<String, Student> key=iterator.next();
System.out.println(key.getKey());
System.out.println(key.getValue().toString());
}
- 遍歷 HashMap 的鍵
- 根據keySet()獲取HashMap的“鍵”的Set集合。
- 通過 Iterator 迭代器遍歷第一步得到的集合
Set<String> keys=map.keySet();
Iterator<String> iterator= keys.iterator();
//當下一個有值時
while(iterator.hasNext()) {
String key=iterator.next();
Student value=map.get(key);
System.out.println(value);
}
- 遍歷 HashMap 的值
- 根據value()獲取HashMap的“值”的集合。
- 通過 Iterator 迭代器遍歷第一步得到的集合
//只能獲取值,不能獲取鍵
Collection<Student> values = map.values();
Iterator<Student> iterator = values.iterator();
//當下一個有值時
while(iterator.hasNext()) {
Student value = iterator.next();
System.out.println(value.toString());
}
底層
HashMap 底層實現採用了哈希表,哈希表的基本結構就是數組加鏈表。我們打開 HashMap 源碼,發現以下兩個內容:
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
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;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
transient Node<K,V>[] table;
......
}
其中 Node[ ] table 就是 HashMap 的核心數組結構,也稱爲 “位桶數組” ,我們在看看 Node 的源碼看看
static class Node<K,V> implements Map.Entry<K,V> {
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;
}
}
一個 Node 對象存儲了:
- key:鍵對象
- value:值對象
- next:下一個結點
- hash:鍵對象的哈希值
顯然每個 Node 對象,代表一條單向鏈表,示意圖如下:
因此我們也得出,整個 Node[ ] 數組的結構(也就是 HashMap )結構如圖:
存儲數據過程 put(key,value)
此處的核心就是產生 hash 值,該值對應數組的存儲位置。如下圖:(其中數組默認大小爲 16)
步驟如下:
-
獲得 key 對象的 hashcode
- 首先調用 key 對象的 hashcode() 方法,獲得 hashcode
-
根據 hashcode 計算出 hash 值(要求在[0,數組長度-1]區間)
- hashcode 是一個整數,我們需要把他轉化爲 [0,數組長度-1] 之間,我們要求轉換後的 hash 值儘量的均勻分佈在 [0,數組長度-1] 這個區間,減少 “hash” 衝突
-
簡單的 hash 算法
- 一種是 hash = hash % 數組長度 ,但是由於除法低效,還有以下算法
- 還有一種是 hash = hash & (數組長度 - 1) 前提是數組的長度是 2 的整數冪
- 示例代碼
public class TestMap { public static void main(String[] args) { int h = 25860399; int length = 16;//length 爲 2 的整數冪,h & (length-1)相當於對 length 取模 myHash(h,length); } private static int myHash(int h,int length) { // TODO Auto-generated method stub System.out.println(h&(length-1)); System.out.println(h%length); return h&(length-1); }
在上述程序,其實發現取餘和位運算結果都是一樣的,事實是爲了獲得更好的散列效果。JDK 對 hashcode 進行了兩次散列處理
4. 生產 Node 對象
5. 將 Node 對象放到 table 數組中
- 如果本 Node 對象的數組索引還沒有 Node 對象,則直接將 Node 對象放進數組,否則,則將已有的 Node 對象的 next 指向本 Node 對象,形成鏈表。
總結上述過程
當添加一個元素 (key-value) 時,首先計算 key 的 hash 值,以此確定插入數組中的位置,但是可能存在同一 hash 值的元素已經被放在數同一位置了,這時就添加到同一 hash 值的元素的後面,他們在數組的同一位置,就形成了鏈表,同-一個鏈表上的Hash值是相同的,所以說數組存放的是鏈表。注意:(JDK8中,當鏈表長度大於8時,鏈表就轉換爲紅黑樹,這樣又大大提高了查找的效率。)
取數據的過程 get(key)
從存儲的過程,我們很容易的退出取數據的過程,如下:
- 獲得 key 的 hashcode ,通過 hash() 散列算法得到 hash 值,進而定位到數組的位置
- 在鏈表上挨個比較 key 對象,調用 equals() 方法,將 key 對象與鏈表上的所有結點 key 對象比較,直到碰到返回 true 結點對象的 value 的值。
- 返回 equals()爲 true 的結點的 value 對象
- 明白了存取過程,在看一下 hashcode 和 equals 的關係:
- java 規定,兩個內容相同的(equals 爲 true)必須具有相等的 hashcode
- 因爲如果 equals 爲 true 而兩個對象的 hashcode 不同,那再整個存儲過程會發生二義性。因爲 hashcode 是用來尋找 在數組中的位置,如果內容相同,數組下標不同,那將會亂套。
爲什麼 Key 值需要重寫 hashCode() 和 equals()
在添加元素,HashMap 的判斷步驟
- 首先會使用當前集合中的每個元素和新添加的元素進行 hash 值比較
- 如果 hash 值不一樣,則添加新的元素
- 如果 hash 值一樣,比較地址值或者使用 equals() 方法進行比較
- 比較結果一樣,則認爲重複不添加
- 所有的比較結果都不一樣則添加
Student stu1= new Student("張三", "10000", 18, "男", "軟件工程", 1000.00);
Student stu2= new Student("張三", "10000", 18, "男", "軟件工程", 1000.00);
HashMap<Student, String> map= new HashMap<>();
map.put(stu1, "我是第一個");
map.put(stu2, "我是第二個");
System.out.println(map.get(stu1));
System.out.println(map.get(stu2));
以上代碼結果爲
我是第一個
我是第二個
明明是同樣的數據,可是爲什麼添加進去了呢,因爲,Object 的 hashcode() 方法是通過對象的地址計算出哈希值,所以不同的對象,地址不同也就添加進去了(equals()也是一樣),爲了區分不同所以得重寫 hashCode() 和 equals() ,因爲 equals() 在 hashCode() 相同的情況下(畢竟 hash 值也是會發生衝突的)通過 equals() 判斷 key 特有的 ID 值來區別不同。
建議:
- 嘗試讓 hashCode 方法的返回值和對象的成員變量有關
- 可以讓 hashCode 方法返回所有成員變量之和。
- 讓基本數據類型直接相加,然後引用數據類型獲取 hashCode 方法返回值後再相加(boolean 不可參與運算
擴容問題
HashMap 的位桶數組,初始大小爲16。實際使用時,顯然大小是可變的。如果位桶數組中的元素達到(0.75*數組length),就重新調整數組大小變爲原來 2 倍大小。
擴容很耗時。擴容的本質是定義新的更大的數組,並將舊數組內容挨個拷貝到新數組中。
JDK8 將鏈表在大於8 情況下變爲紅黑二叉樹
JDK8 中, HashMap 在存儲一個元素時 ,當對應鏈表長度大於 8 時,鏈表就轉換爲紅黑樹,這樣又大大提高了查找的效率。
HashTable
HashTable 和 HashMap 用法幾乎一樣,底層實現也幾乎一樣,只不過 HashTable 的方法添加了 synchronized 關鍵字確保線程同步檢查,效率較低。
HashTable 和 HasbMap 的區別
- HashTable:線程安全,效率低,不也許 key 或 value 爲 null。
- hashMap:線程不安全,效率高,也許 key 或 value 爲 null。
TreeMap
TreeMap 是紅黑二叉樹的典型實現,我們打開 TreeMap 的源碼,發現有一行核心代碼:
private transient Entry<k,v> root = null
root 用來存儲整個樹的根節點,繼續查看 Entry (TreeMap 的內部類) 代碼:
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
......
......
......
}
明顯的看到,存儲了本身數據,左節點,右節點,父節點,以及結點顏色。在內部的方法 put() 和 remove() 方法大量採用了紅黑樹的理論,對於紅黑二叉樹,這裏就不進行深入的討論,有需要的可以參考數據結構的書籍。
TreeMap 和 HashMap 都實現了同樣的接口 Map ,因此對於調用者來說使用沒有區別,HashMap 效率高於 TreeMap ,只是在需要對排序的 Map 才使用 TreeMap 。
Comparable 接口
然而在 TreeMap 是怎樣進行排序的呢?對於個人自定義的類是 TreeMap 是依照什麼依據排序的呢?
在對於已經定義好的 Integer ,String 等系統類, 他們都有實現 Comparable 接口,然後是實現裏面的 compareTo(Emp o)
方法,而對於自定義類,要使得 TreeMap 進行排序,也要實現這個 Comparable 接口。
假設,我們要對一個 Employee 類排序,先依照裏面的 salary 大小,在依照 id 排序,代碼如下:
// 這裏接口中的泛型就是自定義的類,比較要跟自己同類比較
class Employee implements Comparable<Employee> {
int id;
String name;
double salary;
public Employee(int id, String name, double salary) {
super();
this.id = id;
this.name = name;
this.salary = salary;
}
@Override
public String toString() {
return "id:"+id+",name:"+name+",salary:"+salary;
}
//這裏就實現了用於比較自定義類大小的方法
@Override
public int compareTo(Employee o) { //負數:小於,0:等於,正數:大於
if(this.salary>o.salary){
return 1;
}else if(this.salary<o.salary){
return -1;
}else{
if(this.id>o.id){
return 1;
}else if(this.id<o.id){
return -1;
}else{
return 0;
}
}
}
}