我從未見過如此精辟的解說方式,雙列集合框架 Map,看一遍就夠了

1.常用的實現(xiàn)類結(jié)構(gòu)

一、HashMap

實現(xiàn)了Map、Cloneable、Serializable接口,繼承了AbstractMap類

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable    
    /**
     * Map接口: 實現(xiàn)鍵值對,Map接口規(guī)定了一個key對應一個value
     *          HashMap使用該接口用來替換Dictionary類
     *
     * AbstractMap類: 繼承Map的抽象類,減少Map操作的實現(xiàn)
     *
     * Cloneable接口: 可以顯示的調(diào)用Object.clone()方法,合法的對該類
     *                實例進行字段復制
     *
     * Serializable接口: 實現(xiàn)該接口后,該類可以被序列化和反序列化
     */

1.HashMap是否線程安全?

HashMap是線程不安全的,在并發(fā)的環(huán)境下可以使用ConcurrentHashMap。

2.HashMap的內(nèi)部實現(xiàn)

內(nèi)部實現(xiàn):在JDK1.8之前是數(shù)組+鏈表,JDK1.8之后是數(shù)組+鏈表+紅黑樹
加入紅黑樹的原因:JDK1.8之前HashMap使用的是數(shù)組加鏈表,由于哈希函數(shù)不能百分百的讓元素均勻的分布,就會造成有大量的元素存入同一個index(桶)下,這樣index就形成了一條很長的鏈表,由此元素的遍歷的時間復雜度為O(n),失去了HashMap的優(yōu)勢,加入了紅黑樹,查找的時間復雜度為O(log n),實現(xiàn)了優(yōu)化
數(shù)組的特點:查找快,時間復雜度是O(1),增刪慢,時間復雜度是O(n)
鏈表的特點:查找慢,時間復雜度是O(n),增刪快,時間復雜度是O(1)
紅黑樹的特點:在遍歷鏈表的基礎(chǔ)上,紅黑樹查找的時間復雜度是O(log n)
紅黑樹的查詢效率遠大于鏈表,但是插入/刪除要比鏈表慢

3.HashMap的主要參數(shù)

1.默認初始容量:16,必須是2的整數(shù)次方

2.默認加載因子:0.75

3.閾值:容量*加載因子

4.樹形閾值:默認是8,當bucket中的鏈表長度大于8時,則進行鏈表樹化

5.非樹形閾值:默認是6,當進行擴容時,當進行擴容(resize())時(這時候的HashMap的數(shù)據(jù)存儲位置會重新計算),在計算完后,原有的紅黑樹內(nèi)的數(shù)量<6時,則由紅黑樹轉(zhuǎn)換為鏈表

6.樹形最小容量:桶可能是樹的哈希表的最小容量,至少是TREEIFY_THRESHOLD 的 4 倍,這樣能避免擴容時的沖突

//鏈表轉(zhuǎn)紅黑樹的閾值
static final int TREEIFY_THRESHOLD = 8;
//紅黑樹轉(zhuǎn)鏈表的閾值
static final int UNTREEIFY_THRESHOLD = 6;
/**
*最小樹形化容量閾值:即 當哈希表中的容量 > 該值時,才允許樹形化鏈表 (即 將鏈表 轉(zhuǎn)換成紅黑樹)
*否則,若桶內(nèi)元素太多時,則直接擴容,而不是樹形化
*為了避免進行擴容、樹形化選擇的沖突,這個值不能小于 4 * TREEIFY_THRESHOLD
**/
static final int MIN_TREEIFY_CAPACITY = 64;

只有在數(shù)組的長度大于64時,且鏈表的長度>8時,才能樹形化鏈表
鏈表的長度大于8時會調(diào)用treeifyBin方法轉(zhuǎn)換為紅黑樹,但是treeifyBin方法內(nèi)部有一個判斷,當只有數(shù)組的長度>64的時候,才能將鏈表樹形化,否則只進行resize擴容
因為鏈表過長而數(shù)組過短,會經(jīng)常發(fā)生hash碰撞,這時進行樹形化的作用不大,因為鏈表過長的原因就是數(shù)組過短。樹形化之前要檢查數(shù)組的長度,<64進行擴容,而不是進行樹形化
鏈表的長度>8,但數(shù)組的長度<64時,不會進行樹形化,而是進行resize后rehash重新排序

4.HashMap的常用方法

添加:V put(K key,V value) -->添加元素(也可以實現(xiàn)修改)

刪除:void clear() -->清空所有鍵值對元素

? V remove(Object key) -->根據(jù)鍵刪除對應的值,并把值返回

判斷:containsKey(Object key) -->是否包含指定的鍵

? containsValue(Object value)–>是否包含指定的值

遍歷:Set<Map.Entry<K,V>> entrySet()–>獲取鍵值對

? V get(Object key) -->根據(jù)鍵獲取值

? Collection value()–>獲取值的集合

獲?。篠et setKey()–>獲取鍵的集合

? int size()–>獲取集合元素的個數(shù)

基本方法的使用

HashMap<Integer,String> map=new HashMap<>();
        //添加元素
        map.put(1, "a");
        map.put(2, "b");
        map.put(3, "c");
        //鍵不可重復,值被覆蓋
        map.put(3, "C");
        
        //通過鍵刪除整個鍵值對
        map.remove(3);
        //清空
        map.clear();
        //是否為空
        System.out.println(map.isEmpty());//false
        //是否包含4
        System.out.println(map.containsKey(4));//false
        //是否包含“b”值
        System.out.println(map.containsValue("b"));//true
        //獲取集合元素個數(shù)
        System.out.println(map.size());//3
        //通過鍵獲取值
        System.out.println(map.get(3));//C
        //獲取所有值構(gòu)成的集合
        Collection<String> values=map.values();
        for(String v:values){
            System.out.println("值:"+v);//值:a    值:b   值:c
        }
        
        System.out.println(map);
    }

兩種遍歷方式

public static void main(String[] args) {
    Map<String,Integer> map=new HashMap<>();
    map.put("小林",21);
    map.put("小張",35);
    map.put("小王",18);
    demo1(map);
    demo2(map);
  }
  //通過Set<K>  setKey()方法遍歷
  private static void demo1(Map<String,Integer> map) {
    Set<String> keys=map.keySet();
    for (String key:keys){
      Integer value=map.get(key);
      System.out.println("鍵為:"+key+"值為:"+value);//鍵為:小林值為:21
                                                      //鍵為:小王值為:18
                                                      //鍵為:小張值為:35
    }
  }
  //通過Set<Map.Entry<K,V>> entrySet()方法遍歷
  private static void demo2(Map<String, Integer> map) {
    Set<Map.Entry<String,Integer>> kvs=map.entrySet();
    for (Map.Entry<String,Integer> kv:kvs){
      String kstring=kv.getKey();
      Integer istring=kv.getValue();
      System.out.println(kstring+"-----"+istring);
      //小林-----21
      //小王-----18
      //小張-----35
    }
  }

5.關(guān)于hash沖突問題

1.原因:

? 當對某個元素進行哈希運算后,得到一個存儲地址,在進行插入的時候,發(fā)現(xiàn)已經(jīng)被其他元素占用了。這就是所謂的哈希沖突,也叫哈希碰撞。

哈希函數(shù)的設計至關(guān)重要,好的哈希函數(shù)會盡量的確保計算簡單和散列地址分布均勻,但是,數(shù)組是一塊連續(xù)的固定的長度的內(nèi)存空間,再好的哈希函數(shù)也不能保證得到的存儲地址絕對不發(fā)生沖突。
2.解決hash沖突的方法:

開放地址法:發(fā)生沖突,繼續(xù)尋找下一塊未被占用的存儲地址。
再散列函數(shù)法:當發(fā)生沖突,使用第二個、第三個、哈希函數(shù)計算地址,直到無沖突。
鏈地址法:將所有關(guān)鍵字的同義詞的記錄存儲在一個單鏈表中,我們稱這種表為同義詞子表。
HashMap采用的是鏈地址法,也就是數(shù)組+鏈表的方式

HashMap由數(shù)組+鏈表組成的,數(shù)組是HashMap的主體,鏈表則是主要為了解決哈希沖突而存在的,如果定位到的數(shù)組位置不含鏈表(當前的entry的next指向null),那么對于查找,添加等操作很快,僅需一次尋址即可;如果定位到的數(shù)組包含鏈表,對于添加操作,其時間復雜度為O(n),首先遍歷鏈表,存在即覆蓋,否則新增,然后通過key對象的equals方法逐一比對查找。所以!對于性能考慮,HashMap中的鏈表出現(xiàn)越少,性能才會越好。

6.JDK1.8之前HashMap存在死循環(huán)的問題

原因:由于數(shù)組擴容后,同一索引位置的節(jié)點順序會反掉

7.HashMap和Hashtable的區(qū)別


8.重寫equals()方法后,是否一定要重寫hashCode()方法?為什么?

重寫equals()方法,需要重寫hashCode()方法。

hashCode規(guī)定:兩個對象相等(即equals()返回true),hashCode一定相同;兩個對象hashCode相同,兩個對象不一定相等;

重寫equals,而不重寫hashCode方法,默認調(diào)用的是Object類的hashCode()方法,會導致兩個對象的equals相同但hashCode不同。簡單的來說,重寫equals方法后,重寫hashCode方法就是為了確保比較的兩個對象是同一對象。

二、LinkedHashMap

LinkedHashMap的底層結(jié)構(gòu)和HashMap是相同的,因為LinkedHashMap繼承了HashMap,

區(qū)別在于:LinkedHashMap內(nèi)部提供了Entry,替換了HashMap中的Node。

LinkedHashMap:保證在遍歷map元素時,可以照添加的順序?qū)崿F(xiàn)遍歷

原因:在原來的HashMap底層結(jié)構(gòu)的基礎(chǔ)上,增加了一對指針,指向前一個和后一個元素

對于頻繁的遍歷操作,LinkedHashMap的效率要高于HashMap

三、TreeMap

保證照key-value對進行排序,實現(xiàn)排序遍歷,此時考慮key的自然排序或者定制排序,底層使用的是紅黑樹

元素根據(jù)鍵排序,元素具有唯一性

1)自然排序

讓元素所在的類繼承自然排序Comparable

2)比較器排序

讓集合的構(gòu)造方法接收一個比較器接口(Comparator的實現(xiàn)對象)

四、Hashtable

作為古老的實現(xiàn)類;線程安全的,效率低;不能存儲null的key和value

最后

感謝你看到這里,看完有什么的不懂的可以在評論區(qū)問我,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關(guān)技術(shù)文章或行業(yè)資訊,歡迎大家關(guān)注和轉(zhuǎn)發(fā)文章!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容