java基礎中HashMap的原理詳解

HashMap的數據結構:

  • 首先,HashMap中數據的存儲是由數組鏈表一起實現的。
  • 數組是在內存中開闢一段連續的空間,因此,只要知道了數組首個元素的地址,在數組中尋址就會非常容易,其時間複雜度爲O(1)。但是當要插入或刪除數據時,時間複雜度就會變爲O(n)
  • 鏈表是內存中一系列離散的空間,其插入和刪除操作的內存複雜度爲O(1),但是尋址操作的複雜度卻是O(n)。那有沒有一種方法可以結合兩者的優點,即尋址,插入刪除快呢?這個方法就是HashMap。
  • 我們看看HashMap的拉鍊式實現方法。如下圖
    HashMap的拉鍊式實現方法
  • Entry是一個static class,其中包含了key和value,也就是鍵值對,另外還包含了一個next的Entry指針。可以看出:Entry就是數組中的元素,每個Entry其實就是一個key-value對,它特有一個指向下一個元素的引用,這就構成了鏈表

1.HashMap中定義了一個Entry類的數組table

  • 其中table數組就是buckets(桶),其中數組和鏈表的數據區保存的就是一個Entry對象,Entry對象中保存的就是HashMap中的鍵值對(key-value)

2.HashMap中兩個重要的屬性:capacity(容量)和load factor(加載因子)

  • capacitybuckets的容量,load factor是衡量buckets填滿程度的比例。如果對迭代性能要求很高的話不要把capacity設置過大,也不要把load factor設置過小。當buckets(桶)填充的數組(即hashmap中元素的個數)大於capacity*load factor時就要把buckets擴充爲原來的兩倍。

3.put函數(存儲)的實現

  • 先根據key的hashcode()計算出hash值,並用hash值計算出放在數組中的哪個位置
  • 判斷該位置是否爲空,若爲空,則直接調用addEntry()方法將元素加入
  • 若該位置不爲空(產生數據衝突),將這個位置上的元素統一向這個位置所連的鏈表後推一格,然後將要加入的元素放在鏈表頭部(頭插法
  • 如果數據衝突導致鏈表過長,就把鏈表轉換成紅黑樹
  • 如果新添加對象中的key值與之前添加的Entry對象通過equals比較返回true時,新添加Entry的value將覆蓋集合中原有Entry的value,但key不會覆蓋。(保證key的唯一性,不能重複
  • 如果buckets滿了(超過capacity*load factor),就要resize(在resize的過程,簡單的說就是把buckets擴充爲2倍,之後重新計算數組index,把節點再放到新的buckets中)

4.get函數(獲取)的實現

  • 首先計算key的hashcode,找到數組中對應位置的某一元素
  • 如果有衝突,通過key的equals方法在對應位置的鏈表中找到需要的元素
    • 若爲,則在樹中通過key.equals()去查找,O(logn)
    • 若爲鏈表,則在鏈表中通過key.equals()去查找,O(n)
  • 在java1.8之前的是實現中是用鏈表解決衝突的,在產生碰撞的情況下,進行get時,兩步的時間複雜度是O(1)+O(n)。因此,當碰撞很厲害的時候n很大,O(n)的速度顯然是影響速度的。
  • 因此在java1.8中,利用紅黑樹替換鏈表,這樣複雜度就變成了O(1)+O(logn)了,這樣在n很大的時候,能夠比較理想的解決這個問題。

5.案例

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class 測試HashMap {
    public static void main(String[] args) {
        test1();
    }

    public static void test1(){
        HashMap<String, String> map = new HashMap<>();
        map.put("CN", "中華人民共和國");
        map.put("US", "美利堅合衆國");
        map.put("RU", "俄羅斯");
        map.put("FR", "法蘭西共和國");
        map.put("JP", "日本");
        map.put("CN", "中國");//key相同,對前面覆蓋
        map.put("aa","俄羅斯");
        map.put(null, null);

        //1.便利所有的key
        Set<String> set1 = map.keySet();
        for (String i : set1){
            System.out.println(i);
        }
        System.out.println("---------------");
        //2.遍歷所有的values
        Collection<String> values = map.values();
        for (String a : values){
            System.out.println(a);
        }
        System.out.println("---------------");

        //3.遍歷所有的key-value
        /*
            entrySet返回了一個set集合,set集合中放了Entry類型的對象
            Entry中放了key-value
         */
        Set<Map.Entry<String, String>> set = map.entrySet();
        for (Map.Entry<String, String> e : set){
            System.out.println(e.getKey()+"\t\t"+e.getValue());
        }
    }
}


  • 輸出爲:
aa
null
RU
JP
CN
FR
US
---------------
俄羅斯
null
俄羅斯
日本
中國
法蘭西共和國
美利堅合衆國
---------------
aa		俄羅斯
null		null
RU		俄羅斯
JP		日本
CN		中國
FR		法蘭西共和國
US		美利堅合衆國
  • 一般使用第三種遍歷方法

參考資料:

發佈了24 篇原創文章 · 獲贊 12 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章