JavaSE學習筆記(13.Java之集合Map)

1. Map集合

Map集合提供的是一種key-value鍵值對元素的存儲容器,key值不允許重複,重複的key值會導致元素覆蓋!

Map接口提供的能力:

public interface Map<K,V> {

    /*判斷map容器是否爲空*/
    boolean isEmpty();

    /*判斷key是否存在map中*/
    boolean containsKey(Object key);

    /*判斷value是否存在map中*/
    boolean containsValue(Object value);
 
    /*通過key獲取value*/
    V get(Object key);

    /*將key-value鍵值對更新到map中*/
    V put(K key, V value);

    /*將對應key的鍵值對從map中移除*/  
    V remove(Object key);

    /*將Map m的內容全部更新到當前map中去*/
    void putAll(Map<? extends K, ? extends V> m);

    /*清空map*/
    void clear();
    
    /*獲取當前map中所有key值的set集合*/   
    Set<K> keySet();

    /*獲取當前map中所有value值的集合,可以重複*/
    Collection<V> values();

    /*獲取當前map中entry的set集合*/
    Set<Map.Entry<K, V>> entrySet();

    /*判斷兩個map集合是否相等*/
    boolean equals(Object o);

    /*返回map集合的hashCode*/
    int hashCode();
  
    /*Java8,Map接口中新增的default方法*/
    /*默認值爲defaultValue的get方法*/
    default V getOrDefault(Object key, V defaultValue) {
        V v;
        return (((v = get(key)) != null) || containsKey(key))
            ? v
            : defaultValue;
    }
   
    /*指定action函數式接口的foreach*/
    default void forEach(BiConsumer<? super K, ? super V> action) {
        Objects.requireNonNull(action);
        for (Map.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
            action.accept(k, v);
        }
    }
    
    /*通過函數式接口function獲取需要替換到Map中value值*/   
    default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
        Objects.requireNonNull(function);
        for (Map.Entry<K, V> entry : entrySet()) {
            K k;
            V v;
            try {
                k = entry.getKey();
                v = entry.getValue();
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }

            // ise thrown from function is not a cme.
            v = function.apply(k, v);

            try {
                entry.setValue(v);
            } catch(IllegalStateException ise) {
                // this usually means the entry is no longer in the map.
                throw new ConcurrentModificationException(ise);
            }
        }
    }

    /*key值無效或者value爲null,put成功*/
    default V putIfAbsent(K key, V value) {
        V v = get(key);
        if (v == null) {
            v = put(key, value);
        }

        return v;
    }

    /*key,value都匹配刪除成功,否則進行remove行爲*/
    default boolean remove(Object key, Object value) {
        Object curValue = get(key);
        if (!Objects.equals(curValue, value) ||
            (curValue == null && !containsKey(key))) {
            return false;
        }
        remove(key);
        return true;
    }

    /*key,value都匹配成功的時候,進行value值的替換*/
    default boolean replace(K key, V oldValue, V newValue) {
        Object curValue = get(key);
        if (!Objects.equals(curValue, oldValue) ||
            (curValue == null && !containsKey(key))) {
            return false;
        }
        put(key, newValue);
        return true;
    }

    /*key值有效的時候,進行替換,如果是新增key值,不會新建鍵值對*/
    default V replace(K key, V value) {
        V curValue;
        if (((curValue = get(key)) != null) || containsKey(key)) {
            curValue = put(key, value);
        }
        return curValue;
    }

    /*見compute*/
    default V computeIfAbsent(K key,
            Function<? super K, ? extends V> mappingFunction) {
        Objects.requireNonNull(mappingFunction);
        V v;
        if ((v = get(key)) == null) {
            V newValue;
            if ((newValue = mappingFunction.apply(key)) != null) {
                put(key, newValue);
                return newValue;
            }
        }

        return v;
    }

    /*見compute*/
    default V computeIfPresent(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        V oldValue;
        if ((oldValue = get(key)) != null) {
            V newValue = remappingFunction.apply(key, oldValue);
            if (newValue != null) {
                put(key, newValue);
                return newValue;
            } else {
                remove(key);
                return null;
            }
        } else {
            return null;
        }
    }

    /*通過remappingFunction函數式接口對value進行二次計算,然後再進一步操作鍵值對*/
    default V compute(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        V oldValue = get(key);

        V newValue = remappingFunction.apply(key, oldValue);
        if (newValue == null) {
            // delete mapping
            if (oldValue != null || containsKey(key)) {
                // something to remove
                remove(key);
                return null;
            } else {
                // nothing to do. Leave things as they were.
                return null;
            }
        } else {
            // add or replace old mapping
            put(key, newValue);
            return newValue;
        }
    }
    
    /*通過remappingFunction函數式接口,對value進行二次計算,再put*/    
    default V merge(K key, V value,
            BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
        Objects.requireNonNull(remappingFunction);
        Objects.requireNonNull(value);
        V oldValue = get(key);
        V newValue = (oldValue == null) ? value :
                   remappingFunction.apply(oldValue, value);
        if(newValue == null) {
            remove(key);
        } else {
            put(key, newValue);
        }
        return newValue;
    }
}

Java8在Map接口中新增的default方法:

Map接口的遍歷:

public class MapTest
{
    public static void main(String args[])
    {
        Map<String,String> mp = new HashMap<>();
        mp.put("key1","value1");
        mp.put("key2","value2");
        mp.put("key3","value3");

        /*通過Map的keySet遍歷*/
        for (String s : mp.keySet())
        {
            System.out.println("key:" + s + " value:" + mp.get(s));
        }

        /*通過Map的values遍歷*/
        for (String s : mp.values())
        {
            System.out.println("value:" + s);
        }

        /*通過Map的entrySet遍歷*/
        for (Map.Entry<String,String> entry : mp.entrySet())
        {
            System.out.println("key:" + entry.getKey() + " value:" + entry.getValue());
        }

        /*通過Map接口的forEach默認方法*/
        mp.forEach((key,value)->{
            System.out.println("key:" + key + " value:" +value);
        });
    }
}

2. HashMap集合:

2.1 HashMap集合簡述:

  • HashMap是繼承自AbstractMap抽象類,AbstractMap抽象類繼承自Map接口;
  • HashMap是線程不安全集合;
  • HashMap可以接收null元素,key值,value值均可以爲null;
  • HashMap是一個無序的集合;

 

2.2 HashMap的實現原理:

2.2.1 HashMap的原則:

一個key對應一個唯一的value;當key重複的時候,覆蓋原有的value;HashMap的key的唯一性取決於HashCode值相等並且equals返回true!

2.2.2 HashMap的默認配置:

    /*Map容量,默認值爲16,會被自動轉換爲最近的2的N次冪*/
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

    /*Map容器最大容量*/
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /*當達到容量的75%的時候,啓動容器擴容,一次擴一倍*/
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /*Java8後,link長度達到8的時候,滿足鏈表轉紅黑樹條件1*/
    static final int TREEIFY_THRESHOLD = 8;

    /*Java8後,link長度從8降到6的時候,滿足紅黑樹轉換鏈表條件*/
    static final int UNTREEIFY_THRESHOLD = 6;

    /*Java8後,當容器達到64的時候,滿足鏈表轉紅黑樹條件2*/
    static final int MIN_TREEIFY_CAPACITY = 64;

2.2.3 HashMap的存儲形式:

  • Java8以前,HashMap的存儲結構是哈希+鏈表將元素key值的hashcode轉換爲哈希桶的索引;當未發生哈希碰撞的時候,元素直接入桶,當發生哈希碰撞的時候(即哈希桶索引相同、hashcode可能相同,也可能不相同),衝突元素遍歷哈希桶中的鏈表,如果發現hashcode相等且equals相等的元素直接覆蓋,未發現hashcode相等同時equals也相等的元素,將元素插入哈希桶中的鏈表!查找元素的時間複雜度爲O(1)+O(n),如果哈希碰撞嚴重的情況時間複雜度就接近O(n)了,性能不優!所以有了後面的演進!
  • Java8以後,HashMap的存儲結構是哈希+鏈表&紅黑樹,入桶規則和Java8以前相同,但是當Map容器達到64,哈希桶中鏈表元素超過8的時候,會自動將鏈表轉換爲紅黑樹,這樣當哈希碰撞嚴重的時候,查找元素的時間複雜度就接近O(nlogn),比O(n)更優!

Ps:注意HashMap中的哈希碰撞有兩種形式:

  • 一種是hashcode不相等,但是轉換到哈希桶索引的時候,索引相等!這種碰撞由於hashcode不相同,紅黑樹是接近對稱的,查找元素的複雜度接近O(nlogn)!
  • 另一種是hashcode相等時候發生的碰撞,這種碰撞會導致紅黑樹在遍歷相等hashcode的時候更接近一個鏈表的遍歷,因爲相等hashcode的元素會一直在左樹,這樣查找元素的時間複雜度又接近O(n)了!但是可以讓key值支持Comparable接口,支持相同hashcode但是equals不相等的場景的比較,這樣在hashcode相等equals不相等的時候,HashMap會自動根據key對象提供的Comparable能力確認節點在紅黑樹的左邊還是右邊!這樣查找元素的時間複雜度又提升到了O(nlogn)了!

2.2.4 hashcode()方法和equals()方法重寫的注意事項:

  • HashMap實現通過key值對象的hashcode和equals來確認key值的唯一性,兩者均相等的時候,確認爲一個唯一key!
  • hashcode相等,一定會發生哈希碰撞;hashcode不相等,也可能會發生哈希碰撞,只是會降低碰撞的概率!碰撞越多,查詢時,性能越差,所以不同的key值,最好使用不同的hashcode,來降低碰撞概率!
  • 當業務形態中,認爲兩個key值對象equals相等就是相同key的時候;建議重寫equals()的同時一定要同步重寫hashcode(),並且最好equals不相等,hashcode也不相等;equals相等,hashcode也相等,這樣既滿足設計也能保證性能最優!

簡單分析原因(前提條件:當業務形態中,認爲兩個key值對象equals相等就是相同key的時候):

  1. equals相等,hashcode相等:由於equals、hashcode都相等,HashMap會用後來的key元素覆蓋,原有的key元素!(元素位置:相同哈希桶的相同hashcode位置)
  2. equals相等,hashcode不相等:業務側認爲equals相等的,key就是相同的;但是由於hashcode不相等,HashMap會認爲是兩個key,分別都存放在容器裏面,不符合設計預期!(元素位置:在不同的哈希桶或者相同哈希桶的不同hashcode位置)
  3. equals不相等,hashcode相等:由於hashcode相等,equals不相等,在發生哈希碰撞後,哈希桶中的紅黑樹會存在相同hashcode不同equals的節點;再獲取數據的時候,需要遍歷這些相同hashcode不同equals的節點,由於在key不支持Comparable接口的時候,左樹右樹的選擇是通過hashcode判斷的,導致相同hashcode會一直查找左樹;如果這樣的節點數目增多,紅黑樹的查找複雜度就接近鏈表查找的複雜度O(n)了,導致查詢效率降低!(元素位置,在相同哈希桶的不同hashcode位置)
  4. equals不相等,hashcode不相等:由於hashcode不相等,哈希桶中的紅黑樹元素可以通過hashcode直接區分,不存在性能降低的問題,推薦使用!(元素位置,在不同的哈希桶或者相同哈希桶的不同hashcode位置)

2.2.5 HashMap擴容問題:

由於這裏不想對HashMap的resize()進行展開描述,僅僅記錄下Java8以前和Java8以後的實現差異;

  • Java8以前,在數據轉移的時候,使用頭插法,同時併發調用put()方法觸發擴容,可能會造成逆序環形鏈表的問題,造成死鏈!
  • Java8以後,將頭插法改爲了尾插法,不會再出現逆序環形鏈表的問題,不會造成死鏈!

2.2.6 key值的不可變性:

HashMap中的元素位置主要是依賴於key值對象的hashcode和equals進行判斷選擇的,所有針對key值對象的選擇,儘可能選擇不可變對象;如果選擇可變對象,必須保證在元素進入HashMap後,對象不再改變!因爲如果key值對象改變導致hashcode和equals改變了,就會造成通過key值無法獲取到正確元素的情況!

2.2.7 HashMap線程安全性問題討論:

HashMap是個線程不安全的集合,多線程同時操作可能會造成數據異常;如鏈表與紅黑樹相互轉換的時候併發、HashMap擴容的時候併發、鏈表插入讀取的時候併發、紅黑樹插入讀取的時候併發等場景;

如果想讓HashMap支持線程安全,可以使用Collections.synchronizedMap(HashMap)進行包裝!

2.2.8 HashXXX集合中contains()方法、equals()方法的特殊性:

由於HashXXX這類集合的特殊性:通過使用hashcode確定元素位置的方式來提高元素查詢的效率!導致key相等的判斷不僅僅通過equals方法判斷,同時需要hashcode相等!這樣的話HashSetXXX的contains()、HashMapXXX的containsKey()、HashXXX的equals()等涉及到key值判斷的方法,都不再像其他集合那樣僅僅判斷eqauls就可以了,還需要同步判斷hashcode!

2.2.9 hashcode重寫建議:

原則:equals相等的hashcode必須相等,equals不相等的hashcode儘可能不相等!

  1. 將對象中每個有意義的變量計算出一個int類型的hashCode值
  2. 對每個hashCode值乘上一個不同的質數,最後求和

Ps:由於這裏針對HashMap討論比較簡單,並未深入源碼層面,推薦兩篇比較詳細的博文<HashMap看這篇就夠了>   <一文搞定HashMap的實現原理和麪試>

 

3. LinkedHashMap集合:

3.1 LinkedHashMap集合簡述:

LinkedHashMap是HashMap的一個子類,是一個有序的HashMap(可以根據插入順序,遍歷Map);LinkedHashMap通過重寫了HashMap中的newNode()方法,每次調用put插入數據的時候,都會同時將插入順序記錄到一個鏈表中;當需要遍歷Map的時候,通過鏈表中的順序進行遍歷,來實現Map的有序性!

    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }

 

4. Hashtable集合:

4.1 Hashtable集合簡述:

  • Hashtable繼承自Map接口和Dictionary抽象類
  • Hashtable是線程安全集合;
  • Hashtable不可以接收null元素,key值,value值都不可以爲null,否則會報空指針異常;
  • Hashtable是一個無序的集合;

4.2 Hashtable的實現:

  1. 存儲形式:哈希+鏈表,細節實現與Java8以前的HashMap類似
  2. 同步方式:通過synchronized同步方法,實現線程安全

4.3 Hashtable與HashMap比較:

  • HashMap線程不安全,Hashtable線程安全
  • HashMap可以接收null元素,Hashtable不可以接收null元素
  • HashMap中引入紅黑樹提升查詢性能,Hashtable依然使用鏈表存儲哈希桶中的數據
  • HashMap與Hashtable都是無序的Map集合

Ps:由此可以看出,HashMap更加優選於Hashtable(Hashtable的查詢效率很低、同步效率也很低、目前處於半廢棄狀態),如果需要線程安全的HashMap,可以使用Collections.synchronizedMap()進行包裝或者使用ConcurrentHashMap集合!

 

5. TreeMap集合:

5.1 TreeMap集合簡述:

  • TreeMap繼承自Map接口和SortedMap抽象類
  • TreeMap是線程不安全集合;
  • TreeMap的key值不可以爲null,value值可以爲null;
  • TreeMap是一個有序的集合,順序爲key元素排列升序;

5.2 TreeMap的實現:

  • TreeMap不同於XXXHashMap,不是通過Hash桶實現的Key值的存在;TreeMap是一個有序Map,存儲結構爲紅黑樹;
  • TreeMap元素紅黑樹的確認規則:如果TreeMap選擇自然排序(即構造函數中未設置Comparator對象),則通過每個key元素的compareTo方法進行元素大小關係判斷,進而確認元素位置,所以在自然排序的TreeMap每個key元素必須是Comparable接口的實現類!如果TreeMap選擇定製排序,則可以通過自己對Comparator的實現,來決定兩個key元素判斷標準,如果不再通過key元素的compareTo方法進行判斷,key元素可以不再是Comparable的實現類!
  • TreeMap的key元素相等判斷規則:僅僅根據元素的compareTo方法或者Comparator實現來判斷,不使用equals進行判斷;但是爲了保證邏輯的一致性,建議compareTo方法或者Comparator實現邏輯與equals邏輯一致!當compareTo方法返回0的時候,判斷爲兩個key元素相等,與HashMap不同的是,此時僅僅更新value的內容,key元素不再刷新!
public class TreeMapTest
{
	public static void main(String args[])
	{
		/*自然排序*/
		TreeMap<TmKey,String> tmap = new TreeMap<>();
		TmKey key1 = new TmKey(100);
		TmKey key2 = new TmKey(200);
		TmKey key3 = new TmKey(300);
		
		tmap.put(key2, "key2");
		tmap.put(key1, "key1");
		tmap.put(key3, "key3");
		System.out.println(tmap.size());

		for (Map.Entry<TmKey, String> entry : tmap.entrySet())
		{
			System.out.println(entry.getKey().num);
			System.out.println(entry.getValue());
		}
				
		
	}
}

class TmKey implements Comparable<Object>
{
	public int num;
	
	public TmKey(int num)
	{
		this.num = num;
	}
	
	@Override
	public int compareTo(Object obj) {
		
		return 0;

	}
	
	@Override
	public boolean equals(Object obj) {
		
		return true;
    }
	
}

輸出:

Ps:Comparable接口中obj1.compareTo(obj2)方法,返回正整數表示obj1大於obj2;返回負整數表示obj1小於obj2;返回0表示obj1和obj2相等!

5.3 TreeMap的使用:

public class TreeMapTest
{
	public static void main(String args[])
	{
		/*自然排序*/
		TreeMap<TmKey,String> tmap = new TreeMap<>();
		TmKey key1 = new TmKey(100);
		TmKey key2 = new TmKey(200);
		TmKey key3 = new TmKey(300);
		
		tmap.put(key2, null);
		tmap.put(key1, null);
		tmap.put(key3, null);
		System.out.println(tmap.size());

		for (Map.Entry<TmKey, String> entry : tmap.entrySet())
		{
			System.out.println(entry.getKey().num);
		}
		
		/*定製排序*/
		TreeMap<TmKey,String> tmap2 = new TreeMap<>((obj1,obj2)->{
			return obj1.num > obj2.num ? 1 : obj1.num < obj2.num ? -1 : 0;
		});
		
		tmap2.put(key2, null);
		tmap2.put(key1, null);
		tmap2.put(key3, null);
		System.out.println(tmap2.size());

		for (Map.Entry<TmKey, String> entry : tmap2.entrySet())
		{
			System.out.println(entry.getKey().num);
		}
	}
}

class TmKey implements Comparable<Object>
{
	public int num;
	
	public TmKey(int num)
	{
		this.num = num;
	}
	
	@Override
	public int compareTo(Object obj) {
		
		TmKey m = (TmKey)obj;
		
		return this.num > m.num ? 1 : this.num < m.num ? -1 : 0;

	}
	
	public boolean equals(Object obj) {
		
		if (obj == this)
		{
			return true;
		}
		if (obj != null && obj.getClass() == TmKey.class)
		{
			TmKey m = (TmKey)obj;
			return m.num == this.num;
		}
		return false;
    }
	
}

Ps:通過上面TreeMap自然排序和定製排序的例子,可以說明TreeMap的遍歷輸出都是升序輸出!

5.4 TreeMap提供的方法:

TreeMap類中除了實現了Map接口中的方法,由於它Key元素的有序性,還提供了大量的關於Entry和Key數值比較相關的方法,此處就不再展開贅述了!

常用方法:

 

6. WeakHashMap集合:

WeakHashMap與HashMap基本完全相同,只是HashMap的key保留對象實例的強引用;WeakHashMap的key保留對象的弱引用!當key被Jvm垃圾回收機制回收後,WeakHashMap會自動清理該key的鍵值對!

 

7. IdentityHashMap集合:

IdentityHashMap與HashMap基本完全相同,只是HashMap判斷key相等的條件是hashcode相等同時equals相等;IdentityHashMap判斷key相等的條件必須是兩個key爲同一個對象實例(key1==key2)!

 

8. EnumMap集合:

  • EnumMap繼承自Map接口;
  • EnumMap是線程不安全集合;
  • EnumMap的key值不可以爲null,value值可以爲null;
  • EnumMap是一個有序的集合,集合順序爲key元素枚舉值的數值升序;
  • EnumMap中的key元素只能是同一類枚舉類型的枚舉實例,EnumMap構造的時候已經確定了key值元素的枚舉類型,如果類型不一致則拋出異常!
  • EnumMap是通過位運算進行存儲,有着佔用內存小,操作效率的優勢;但是侷限爲key值只能是枚舉類型!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章