中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java數據結構之HashMap和HashSet源碼分析

發布時間:2023-03-25 10:05:30 來源:億速云 閱讀:176 作者:iii 欄目:開發技術

本篇內容介紹了“Java數據結構之HashMap和HashSet源碼分析”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

    1、認識 HashMap 和 HashSet

    Java數據結構之HashMap和HashSet源碼分析

    TreeMap 和 TreeSet實現了 SortedMap 和 SortedSet 接口(本質是 實現了 NavigableMap 和 NavigableSet),表示你創建的 TreeMap 或 TreeSet,必須是可排序的,也就是里面的元素是可比較的。

    因為 Set 的底層就是 Map,那么這里我們就來驗證下 TreeSet 關于 key 有序,而 HashSet 不關于 key 有序。

    public class Test {
        public static void main(String[] args) {
            Set<String> treeSet = new TreeSet<>();
            Set<String> hashSet = new HashSet<>();;
            treeSet.add("0012");
            treeSet.add("0083");
            treeSet.add("0032");
            treeSet.add("0057");
            System.out.print("TreeSet: ");
            for (String s : treeSet) {
                System.out.print(s + " ");
            }
            hashSet.add("0012");
            hashSet.add("0083");
            hashSet.add("0032");
            hashSet.add("0057");
            System.out.print("\nHashSet: ");
            for (String s : hashSet) {
                System.out.print(s + " ");
            }
        }
    }

    打印結果

    Java數據結構之HashMap和HashSet源碼分析

    2、哈希表

    2.1 什么是哈希表

    之前的學習中,如果我們要查找一個元素,肯定是要經過比較的,那有沒有一種辦法,可以不用經過比較,直接就能拿到呢?

    如果我們能構造一種存儲結構,通過一種函數 (hashFunc) 使元素的存儲位置與函數得出的關鍵碼之間能夠建立一一映射的關系,那么在查找某個元素的時候,就能通過這個函數來很快的找到該元素所在的位置。

    這種函數就叫做哈希(散列)函數,上述所說構造出來的結構,叫做哈希表或者稱為散列表。

    Java數據結構之HashMap和HashSet源碼分析

    插入元素的時候:根據元素的關鍵碼,Person中關鍵碼可能是 age,這個具體情況具體分析,上述例子只是在插入整型的時候,通過關鍵碼通過哈希函數得到的 index 就是要插入的位置。

    搜索元素的時候:對元素的關鍵碼,通過哈希函數得出的 index 位置,與該位置的關鍵碼比較,若倆個關鍵碼相同,則搜索成功。

    采取上面的方法,確實能避免多次關鍵碼的比較,搜索的效率也提高的,但是問題來了,拿上述圖的情況來舉例子的話,我接著還要插入一個元素 15,該怎么辦呢?

    2.2 哈希沖突

    2.2.1 概念

    接著上面的例子來說,如果要插入 15,使用哈希函數出來的 index = 5,但是此時的 5,下標的位置已經有元素存在了,這種情況我們就稱為哈希沖突!

    簡單來說,不同的關鍵字通過相同的哈希函數計算出相同的哈希地址(前面我們稱為 index),這種現象就被稱為哈希沖突或哈希碰撞! 

    2.2.2 設計合理哈希函數 - 避免沖突

    如果哈希函數設計的不夠合理,是可能會引起哈希沖突的。

    哈希函數的定義域,必須包括所需存儲的全部關鍵碼,哈希函數計算出來的地址,應能均勻的分布在整個空間中,哈希函數不能設計太過于復雜。(工作中一般用不著我們親自設計)

    常見的哈希函數:

    直接定制法:取關鍵字的某個線性函數作為哈希地址:hash = A * key + B, 這樣比較簡單,但是需要事先知道關鍵字的分布情況,適合于查找比較小且連續的情況。

    除留余數法:設哈希表中允許的地址數為 m,取一個不大于 m 的數,但接近或等于 m 的質數 p 作為除數,即哈希函數:hash = key % p,(p <= m)。

    還有其他的方法感興趣可以查閱下相關資料,這兩個只是比較常見的方法了,當然,就算哈希函數設計的再優秀,只是產生哈希的沖突概率沒那么高,但是仍然避免不了哈希沖突的問題,于是又有了一個降低沖突概率的辦法,調節負載因子。

    2.2.3 調節負載因子 - 避免沖突

    負載因子 &alpha; = 哈希表中元素個數 / 哈希表的長度

    哈希表的長度沒有擴容是定長的,即 &alpha; 與 元素的個數是成正比的,當 &alpha; 越大,即代碼哈希表中的元素個數越多,元素越多,發生哈希沖突的概率就增加了,因此 &alpha; 越小,哈希沖突的概率也就越小。所以我們應該嚴格控制負載因子的大小,在 Java 中,限制了負載因子最大為 0.75,當超過了這個值,就要進行擴容哈希表,重新哈希(重新將各個元素放在新的位置上)。

    所以負載因子越大,沖突率越高,我們就需要降低負載因子來變相的降低沖突率,哈希表中元素個數不能改變,所以只能給哈希表擴容了。

    2.2.4 Java中解決哈希沖突 - 開散列/哈希桶

    上面講述的都是避免沖突的方法,其實還往往不夠,不管怎么避免,還是會有沖突的可能性,那么通常我們解決哈希沖突有兩種辦法,分別是 閉散列 和 開散列 那么今天我們主要介紹 Java 中的開散列是如何解決的,感興趣可以下來自己了解下閉散列。

    開散列又叫鏈地址法,或開鏈法,其實簡單來說就是一個數組加鏈表的結構。如圖:

    Java數據結構之HashMap和HashSet源碼分析

    如上圖我們可得出,通過哈希函數得出的地址相同的關鍵碼放在一起,這樣的每個子集合稱為桶,接著桶中的元素用鏈表的結構組織起來,,每個鏈表的頭節點存放在哈希表中。

    3、HashMap 的部分源碼解讀

    3.1 HashMap 的構造方法

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
     
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
     
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
     
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                    initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                    loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

    這幾個構造方法相信都不難理解,第一個無參構造方法,默認負載因子是 0.75,第二個構造方法是你可以傳一個 Map 進去,構造出一個新的 Map,負載因子仍然是默認的 0.75, 第三個構造方法就是指定開辟的容量了(并不是你想象那樣簡單哦,面試題講解),這個很簡單,第四個構造方法指定容量并且自定義負載因子。

    在 HashMap 中,實例化對象的時候并不會默認開辟空間(指定空間除外)。

    3.2 HashMap 是如何插入元素的?

    前面對 HashMap 的講解,已經大概了解了一點,但是 HashMap 底層的哈希函數是如何設定的呢?而且 Map 中不能有重復值的情況,是利用什么來判斷這兩個值是相同的值呢?

    這里我們通過查看 put 方法的源碼,來帶大家一層層揭開這個面紗:

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    進入到 put 方法,隨之調用了 putVal 方法,而第一個參數就是我們哈希函數的實現了,我們轉到hash() 方法中:

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    通過哈希函數,能看出,首先調用 key 的hashCode 方法,接著無符號右移 16 位,即將 key 的高16位 與 低 16位 異或得到的 hash 值,通過這個也就在說明,我們如果是自定義類型的話,比如 Person,那么一定要重寫 hashCode 方法,不然則是調用 Object 里的 hashCode 方法了。

    接著我們再往下看,putVal 方法里面的邏輯,這里代碼比較長,我們分段演示:

    Java數據結構之HashMap和HashSet源碼分析

    這里就是判斷哈希表是否為有元素,如果表為 null 或者 哈希表的長度為 0,就會初始化數組(Node類型的數組),即調用 resize() 方法。初始哈希表的長度是 16,臨界閾值為 16 * 0.75 = 12,也就是數組元素達到 12個即會擴容。往后走代碼:

    Java數據結構之HashMap和HashSet源碼分析

    這里作用是通過 hash 值得到索引 i,也就是數組的下標,判斷這個位置是否有元素了,如果沒有則直接 new 一個節點放到該處。反之走 else 就表示該數組下標已經有元素了。

    Java數據結構之HashMap和HashSet源碼分析

    如果得到的 i 索引處有元素,則需要判斷當前下標元素的 hash 值是否與我們要插入的元素 hash 值相同,如果相同,接著判斷 下標元素的 key 與我們要插入元素的 key一樣,或者通過 equals 方法比較是一樣了,則表示是同一個元素,則不用插入了,e 保存這個已經存在的元素。到這里也能發現,這其實就是 Map 底層不能有重復 key 的原因了。

    那如果他們不相同的情況下,就會判斷該下標 i 的位置值是不是紅黑樹的節點(到了一定條件哈希桶中的元素會采用紅黑樹來組織數據),如果是,則采用紅黑樹的方式進行插入元素,這里我們就不往里面看了。

    最后都不滿足的情況,也就是元素不相同,并且不是紅黑樹結構,則走后面的 else,首先這個 else 里面是個死循環,要想退出,必須滿足兩個條件,① 找到了可以插入的地方,② 在哈希桶中發現了相同的元素。

    比較該數組索引 i 下標的下一個節點,如果為 null,則直接插入該節點,如果鏈表長度大于8,則可能需要樹化,也就是轉換成紅黑樹,如果找到了相同的 key,則不用插入直接跳出循環。

    上面的 else 走完后,如果存在添加相同的元素的時候,e 則不為 null,只需要將待添加元素的 value 值賦值給原本存在元素的 value 值即可,也就是更新一下元素的 value 值。afterNodeAccess 方法不用管,是使用 LinkedHashMap 實現的一個操作,這里并沒有使用。

    最后部分:

    Java數據結構之HashMap和HashSet源碼分析

    這里有效元素個數自增,如果超過了數組長度,則重新判斷是否擴容(兩倍擴容)。 afterNodeInsertion 也不用管,LinkedHashMap中的,這里也沒有實際用途。

    總結:HashMap 的 put 方法,是根據 key 的 hashCode 來計算 hash 值的,即我們要在自定義類型中重寫 HashCode 方法,再者,是根據 equals 來判斷 key 是否相同,也表示著我們同時需要去重寫 equals 方法。

    3.3 哈希表下的鏈表,何時會樹化?

    在上述講解 put 方法的時候,發現插入元素的時候,數組索引位置的鏈表不止一個元素的時候會判斷是否要轉換成紅黑樹,那么具體要達到什么條件才能轉換成紅黑樹呢?我們直接看上述的 treeifyBin 方法即可。

    Java數據結構之HashMap和HashSet源碼分析

    樹化的前提是,鏈表的長度大于等于 8,就會樹化,因為是從 binCount 是從 0 開始的,所以 TREEIFY_THRESHOLD - 1。那么鏈表的長度大于等于 8,一定會從鏈表變成紅黑樹嗎?我們往后看:

    Java數據結構之HashMap和HashSet源碼分析

    第一個 if 當哈希表為空,或者表(數組)的長度小于64,只進行擴容即可,不會轉換成樹,當哈希表的長度大于 64,才有可能轉換成紅黑樹。

    所以我們最終得出,HashMap 中哈希表下的鏈表,當鏈表長度大于等于 8,并且哈希表的長度大于等于 64 則會將此鏈表轉換成紅黑樹。 

    4、相關面試題

    4.1 鏈表轉換成紅黑樹如何比較?

    前面我們學過 TreeMap TreeSet 底層就是紅黑樹,里面的元素必須是可比較的,那哈希表下的鏈表轉換成紅黑樹之后,沒有重寫 compareTo 怎么辦呢?這里會按照 key 的哈希值來進行比較,所以就算轉換成紅黑樹后,也不會關于 key 有序,再者可能只是哈希表其中一個索引位置下的結構是紅黑樹,其他的仍然可能是鏈表。

    4.2 hashCode 和 equals 的區別

    hashCode 是虛擬出對象的地址,底層是 native 封裝的,即 C/C++實現的,而 equals 則是比較兩個對象是否相同。

    當我們重寫 hashCode 的時候,是調用 Objects 里面的 hash 方法:

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    舉個例子,person1 和 person2 兩個對象的 hashCode 完全不一樣,但通過 hash 函數得到的 hash 值是相同的!而 hashCode 也是通過 hash 出來的,即對象的 hashCode 可以稱為 hash 值,所以得出,兩個對象的 hashCode 相同,但是這兩個對象不一定相同。

    對于 persong1.equals(person2) 的值為 true 的情況,則代表這兩個對象里面的值都是一樣的,所以 equals 為 ture,兩個一模一樣的對象,最終的 hash 值肯定也是一樣的,并且 HashMap 也是調用 key 中的 equals() 方式來進行判斷是否有重復的 key 值。

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age &&
                Objects.equals(name, person.name);
    }

    總結:

    兩個對象的 HashCode 相同,不代表這兩個對象完全相同。

    兩個對象的 equals 的結果為 true,則代表這兩個對象完全相同。

    4.3 以下代碼分配的內存是多少?

    public static void main(String[] args) {
        Map<Integer, Integer> map = new HashMap<>(25);
    }

    如果你天真的回答是 25 個數組元素大小,那就錯了,我們點進去構造方法,發現是調用另一個構造方法,在接著點進去,看到最后一行代碼即 tableSizeFor 方法:

    Java數據結構之HashMap和HashSet源碼分析

    這里就沒必要慢慢算了,實際就是給你找到一個離 cap 最近的 2 的 n 次方數,取值范圍得大于等于 cap,例如上述 25 則實際開辟的就是 1  2  4  8  16  32  64....,那么上述代碼實際開辟的大小就是 32 個數組空間大小。

    5、性能分析

    雖然哈希表一直在和沖突做斗爭,但在實際使用過程中,我們認為哈希表的沖突率是不高的,沖突個數是可控的, 也就是每個桶中的鏈表的長度是一個常數,所以,通常意義下,我們認為哈希表的插入/刪除/查找時間復雜度是 O(1) 。 

    “Java數據結構之HashMap和HashSet源碼分析”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    泰和县| 文登市| 庄浪县| 兴城市| 西乌珠穆沁旗| 北川| 宜宾县| 南昌县| 乐都县| 鸡东县| 井陉县| 华安县| 泾阳县| 子长县| 五大连池市| 阜康市| 绥中县| 蓬莱市| 南靖县| 临高县| 南京市| 团风县| 汕尾市| 固原市| 织金县| 定西市| 邓州市| 花莲县| 石狮市| 郸城县| 岫岩| 肥东县| 上杭县| 南皮县| 中宁县| 治多县| 泰宁县| 吴忠市| 霍城县| 三亚市| 安图县|