Map集合

概述

在現實生活中,我們要存儲某些信息的時候,比如,我們使用的微信就是一個手機號對應一個微信賬戶,這是一種成對的存儲關係。
Map 就是用來存儲“鍵(key) 值(value)”對的。通過鍵識別,所以鍵對象不能重複。

對比 Collection 中的集合,Map 集合中的元素是成對存在,每個元素由鍵與值兩部分組成,通過鍵找到所對應的值。

注意的是:Map 集合中不能包含重複的鍵,值可以重複;每個鍵對應一個值。(如果重複了鍵將會覆蓋舊的值,重複是根據 equals 方法判斷)

常用類

Map 中常用的子類爲 HashMap ,HashTable,TreeMap等。

HashMap<K,V>

存儲數據採用的哈希表結構,所以是無序的,並且爲了鍵的唯一性,不重複,需要重寫鍵的 hashCode() 方法,equals() 方法

  • 概述
    • 他是一個散列表,對比Collection 的單列集合,它是一個雙列集合。存儲內容是鍵值對(key-value)一對一的關係映射。 並且線程是不同步的,可以存入 null 鍵,null 值。

遍歷方式

  1. 遍歷 HashMap 的鍵值對
    1. 根據entrySet()獲取 HashMap 的“鍵值對”的 Set 的集合
    2. 通過 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());
		}
  1. 遍歷 HashMap 的鍵
    1. 根據keySet()獲取HashMap的“鍵”的Set集合。
    2. 通過 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);
		}
  1. 遍歷 HashMap 的值
    1. 根據value()獲取HashMap的“值”的集合。
    2. 通過 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 對象,代表一條單向鏈表,示意圖如下:
image
因此我們也得出,整個 Node[ ] 數組的結構(也就是 HashMap )結構如圖:
image

存儲數據過程 put(key,value)

此處的核心就是產生 hash 值,該值對應數組的存儲位置。如下圖:(其中數組默認大小爲 16)
image
步驟如下:

  1. 獲得 key 對象的 hashcode

    • 首先調用 key 對象的 hashcode() 方法,獲得 hashcode
  2. 根據 hashcode 計算出 hash 值(要求在[0,數組長度-1]區間)

    • hashcode 是一個整數,我們需要把他轉化爲 [0,數組長度-1] 之間,我們要求轉換後的 hash 值儘量的均勻分佈在 [0,數組長度-1] 這個區間,減少 “hash” 衝突
  3. 簡單的 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)

從存儲的過程,我們很容易的退出取數據的過程,如下:

  1. 獲得 key 的 hashcode ,通過 hash() 散列算法得到 hash 值,進而定位到數組的位置
  2. 在鏈表上挨個比較 key 對象,調用 equals() 方法,將 key 對象與鏈表上的所有結點 key 對象比較,直到碰到返回 true 結點對象的 value 的值。
  3. 返回 equals()爲 true 的結點的 value 對象
  4. 明白了存取過程,在看一下 hashcode 和 equals 的關係:
    • java 規定,兩個內容相同的(equals 爲 true)必須具有相等的 hashcode
    • 因爲如果 equals 爲 true 而兩個對象的 hashcode 不同,那再整個存儲過程會發生二義性。因爲 hashcode 是用來尋找 在數組中的位置,如果內容相同,數組下標不同,那將會亂套。
爲什麼 Key 值需要重寫 hashCode() 和 equals()

在添加元素,HashMap 的判斷步驟

  1. 首先會使用當前集合中的每個元素和新添加的元素進行 hash 值比較
  2. 如果 hash 值不一樣,則添加新的元素
  3. 如果 hash 值一樣,比較地址值或者使用 equals() 方法進行比較
  4. 比較結果一樣,則認爲重複不添加
  5. 所有的比較結果都不一樣則添加
        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 的區別

  1. HashTable:線程安全,效率低,不也許 key 或 value 爲 null。
  2. 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;
			}
		}
		
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章