3 手寫Java HashMap核心源碼


手寫Java HashMap核心源碼

上一章手寫LinkedList核心源碼,本章我們來手寫Java HashMap的核心源碼。
我們來先了解一下HashMap的原理。HashMap 字面意思 hash + map,map是映射的意思,HashMap就是用hash進行映射的意思。不明白?沒關系。我們來具體講解一下HashMap的原理。

HashMap 使用分析

//1 存
HashMap<String,String> map = new HashMap<>();
map.put("name","tom");

//2 取
System.out.println(map.get("name"));//輸出 tom

使用就是這么簡單。

HashMap 原理分析

我們知道,Object類有一個hashCode()方法,返回對象的hashCode值,可以理解為返回了對象的內存地址,暫且不管返回的是內存地址或者其它什么也好,先不管,至於hashCode()方法回返的數是怎么算的?我們也不管

第1 我們只需要記住:這個函數返回的是一個數就行了。
第2 HashMap內部是用了一個數組來存放數據

1 HashMap是如何把 name,tom存放的?
下面我們用一張圖來演示

從上圖可以看出:
注:上圖中數組的大小是7,是多少都行,只是我們這里就畫了7個元素,我們就以數組大小為7來說明HashMap的原理。

  1. 數組的大小是7,那么數組的索引范圍是[0 , 6]
  2. 取得key也就是"name"的hashCode,這是一個數,不管這個數是多少,對7進行取余數,那么范圍肯定是 [0 , 6],正好和數組的索引是一樣的。
  3. "name".hashCode() % 7 的值假如為2 ,那么value也就是"tom"應該存放的位置就是2
  4. data[2] = "tom" ,存到數組中。是不是很巧妙。

2 下面再來看看如何取?
也用一張圖來演示底層原理,如下

由上圖可知:

  1. 首先也是獲取key也就是"name"的hashCode值
  2. 用hashCode值對數組的大小 7 進行取余數,和存的時候運行一樣,肯定也是2
  3. 從數組的第 2 個位置把value取出,即: String value = data[2]

注:有幾點需要注意

  1. 某個對象的hashCode()方法返回的值,在任何時候調用,返回的值都是一樣的
  2. 對一個數 n 取余數 ,范圍是 [ 0, n - 1 ]

注:有幾個問題需要解決

  1. 存的時候,如果不同的key的hashCode對數組取余數,都正好相同了,也就是都映射在了數組的同一位置,怎么辦?這就是hash沖突問題
    比如 9 % 7 == 2 , 16 % 7 == 2都等於2
    答:數組中存放的是一個節點的數據結構,節點有next屬性,如果hash沖突了,單鏈表進行存放,取的時候也是一樣,遍歷鏈表

  2. 如果數組已經存滿了怎么辦?
    答:和ArrayList一樣,進行擴容,重新映射

  3. 直接使用hashCode()值進行映射,產生hash沖突的概論很大,怎么辦?
    答:參考JDK中HashMap中的實現,有一個hash()函數,再對hashCode()的值進行運行一下,再進行映射

由上可知:HashMap是用一個數組來存放數據,如果遇到映射的位置上面已經有值了,那么就用鏈表存放在當前的前面。數組+鏈表結構,是HashMap的底層結構
假如我們的數組里面存放的元素是QEntry,如下圖:

手寫 HashMap 核心源碼

上面分析了原理,接下來我們用最少的代碼來提示HashMap的原理。
我們就叫QHashMap類,同時數組里面的元素需要也需要定義一個類,我們定義在QHashMap類的內部。就叫QEntry

QEntry的定義如下:

    //底層數組中存放的元素類
   public static class QEntry<K, V> {
        K key;      //存放key
        V value;    //存放value
        int hash;   //key對應的hash值
        
        //hash沖突時,也就是映射的位置上已經有一個元素了
        //那么新加的元素作為鏈表頭,已經存放的放在后面
        //即保存在next中,一句話:添加新元素時,添加在表頭
        QEntry<K, V> next;  

        public QEntry(K key, V value, int hash, QEntry<K, V> next) {
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.next = next;
        }
    }

QEntry類的定義有了,下面看下QHashMap類中需要哪些屬性?
QHashMap類的定義如下圖:

public class QHashMap<K, V> {
    //默認的數組的大小
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    //默認的擴容因子,當數據中元素的個數越多時,hash沖突也容易發生
    //所以,需要在數組還沒有用完的情況下就開始擴容
    //這個 0.75 就是元素的個數達到了數組大小的75%的時候就開始擴容
    //比如數組的大小是100,當里面的元素增加到75的時候,就開始擴容
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //存放元素的數組
    private QEntry[] table;

    //數組中元素的個數
    private int size;
 
    ......
}    

只需要兩個常量和兩個變量就夠了。
下面我們看下QHashMap的構造函數,為了簡單,只實現一個默認的構造函數

  public QHashMap() {
        //創建一個數組,默認大小為16
        table = new QEntry[DEFAULT_INITIAL_CAPACITY];
        
        //此時元素個數是0
        size = 0;
    }

我們來看下QHashMap是如何存放數據的 map.put("name","tom")
put()函數的實現如下:

    /**
     * 1 參數key,value很容易理解
     * 2 返回V,我們知道,HashMap有一個特點,
     * 如果調用了多次 map.put("name","tom"); map.put("name","lilei");
     * 后面的值會把前面的覆蓋,如果出現這種情況,返回舊值,在這里返回"tom"
     */
    public V put(K key, V value) {
        //1 為了簡單,key不支持null
        if (key == null) {
            throw new RuntimeException("key is null");
        }

        //不直接用key.hashCode(),我們對key.hashCode()再作一次運算作為hash值
        //這個hash()的方法我是直接從HashMap源碼拷貝過來的。可以不用關心hash()算法本身
        //只需要知道hash()輸入一個數,返回一個數就行了。
        int hash = hash(key.hashCode());

        //用key的hash值和數組的大小,作一次映射,得到應該存放的位置
        int index = indexFor(hash, table.length);

        //看看數組中,有沒有已存在的元素的key和參數中的key是相等的
        //相等則把老的值替換成新的,然后返回舊值
        QEntry<K, V> e = table[index];
        while (e != null) {
            //先比較hash是否相等,再比較對象是否相等,或者比較equals方法
            //如果相等了,說明有一樣的key,這時要更新舊值為新的value,同時返回舊的值
            if (e.hash == hash && (key == e.key || key.equals(e.key))) {
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
            e = e.next;
        }

        //如果數組中沒有元素的key與傳的key相等的話
        //把當前位置的元素保存下來
        QEntry<K, V> next = table[index];

        //next有可能為null,也有可能不為null,不管是否為null
        //next都要作為新元素的下一個節點(next傳給了QEntry的構造函數)
        //然后新的元素保存在了index這個位置
        table[index] = new QEntry<>(key, value, hash, next);

        //如果需要擴容,元素的個數大於 table.length * 0.75 (別問為什么是0.75,經驗)
        if (size++ >= (table.length * DEFAULT_LOAD_FACTOR)) {
            resize();
        }

        return null;
    }

注釋很詳細,這里有幾個函數
hash()函數是直接從HashMap源碼中拷貝的,不用糾結這個算法。
indexFor(),傳入hash和數組的大小,從而知道我們應該去哪個位置查找或保存
這兩個函數的源碼如下:

   //對hashCode進行運算,JDK中HashMap的實現,直接拷貝過來了
    static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    //根據 h 求key落在數組的哪個位置
    static int indexFor(int h, int length) {
        //或者  return h & (length-1) 性能更好
        //這里我們用最容易理解的方式,對length取余數,范圍就是[0,length - 1]
        //正好是table數組的所有的索引的范圍

        h = h > 0 ? h : -h; //防止負數

        return h % length;
    }

還有一個擴容函數。當元素的個數大於 table.length * 0.75時,我們就開始擴容
resize()的源碼如下 :

  //擴容,元素的個數大於 table.length * 0.75
    //數組擴容到原來大小的2倍
    private void resize() {
        //新建一個數組,大小為原來數組大小的2倍
        int newCapacity = table.length * 2;
        QEntry[] newTable = new QEntry[newCapacity];

        QEntry[] src = table;

        //遍歷舊數組,重新映射到新的數組中
        for (int j = 0; j < src.length; j++) {
            //獲取舊數組元素
            QEntry<K, V> e = src[j];

            //釋放舊數組
            src[j] = null;

            //因為e是一個鏈表,有可能有多個節點,循環遍歷進行映射
            while (e != null) {
                //把e的下一個節點保存下來
                QEntry<K, V> next = e.next;

                //e這個當前節點進行在新的數組中映射
                int i = indexFor(e.hash, newCapacity);

                //newTable[i] 位置上有可能是null,也有可能不為null
                //不管是否為null,都作為e這個節點的下一個節點
                e.next = newTable[i];

                //把e保存在新數組的 i 的位置
                newTable[i] = e;

                //繼續e的下一個節點的同樣的處理
                e = next;
            }
        }

        //所有的節點都映射到了新數組上,別忘了把新數組的賦值給table
        table = newTable;
    }

相比put()函數來說,get()就簡單多了。
只需要通過hash值找到相應的數組的位置,再遍歷鏈表,找到一個元素里面的key與傳的key相等就行了。
put()方法的源碼如下:

    //根據key獲取value
    public V get(K key) {

        //同樣為了簡單,key不支持null
        if (key == null) {
            throw new RuntimeException("key is null");
        }

        //對key進行求hash值
        int hash = hash(key.hashCode());

        //用hash值進行映射,得到應該去數組的哪個位置上取數據
        int index = indexFor(hash, table.length);

        //把index位置的元素保存下來進行遍歷
        //因為e是一個鏈表,我們要對鏈表進行遍歷
        //找到和key相等的那個QEntry,並返回value
        QEntry<K, V> e = table[index];
        while (e != null) {

            //比較 hash值是否相等
            if (hash == e.hash && (key == e.key || key.equals(e.key))) {
                return e.value;
            }
                
            //如果不相等,繼續找下一個    
            e = e.next;
        }

        return null;
    }

上面就是QHashMap的核心源碼,我們沒有實現刪除。
下面是把QHashMap整個類的源碼發出來

QHashMap完整源碼如下:

public class QHashMap<K, V> {
    //默認的數組的大小
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    //默認的擴容因子,當數組的大小大於或者等於當前容量 * 0.75的時候,就開始擴容
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //底層用一個數組來存放數據
    private QEntry[] table;

    //數組大小
    private int size;

    //一個點節,數組中存放的單位
    public static class QEntry<K, V> {
        K key;
        V value;
        int hash;
        QEntry<K, V> next;

        public QEntry(K key, V value, int hash, QEntry<K, V> next) {
            this.key = key;
            this.value = value;
            this.hash = hash;
            this.next = next;
        }
    }

    public QHashMap() {
        table = new QEntry[DEFAULT_INITIAL_CAPACITY];
        size = 0;
    }

    //根據key獲取value
    public V get(K key) {

        //同樣為了簡單,key不支持null
        if (key == null) {
            throw new RuntimeException("key is null");
        }

        //對key進行求hash值
        int hash = hash(key.hashCode());

        //用hash值進行映射,得到應該去數組的哪個位置上取數據
        int index = indexFor(hash, table.length);

        //把index位置的元素保存下來進行遍歷
        //因為e是一個鏈表,我們要對鏈表進行遍歷
        //找到和key相等的那個QEntry,並返回value
        QEntry<K, V> e = table[index];
        while (e != null) {

            //比較 hash值是否相等
            if (hash == e.hash && (key == e.key || key.equals(e.key))) {
                return e.value;
            }
            
            //如果不相等,繼續找下一個    
            e = e.next;
        }

        return null;
    }

    /**
     * 1 參數key,value很容易理解
     * 2 返回V,我們知道,HashMap有一個特點,
     * 如果調用了多次 map.put("name","tom"); map.put("name","lilei");
     * 后面的值會把前面的覆蓋,如果出現這種情況,返回舊值,在這里返回"tom"
     */
    public V put(K key, V value) {
        //1 為了簡單,key不支持null
        if (key == null) {
            throw new RuntimeException("key is null");
        }

        //不直接用key.hashCode(),我們對key.hashCode()再作一次運算作為hash值
        //這個hash()的方法我是直接從HashMap源碼拷貝過來的。可以不用關心hash()算法本身
        //只需要知道hash()輸入一個數,返回一個數就行了。
        int hash = hash(key.hashCode());

        //用key的hash值和數組的大小,作一次映射,得到應該存放的位置
        int index = indexFor(hash, table.length);

        //看看數組中,有沒有已存在的元素的key和參數中的key是相等的
        //相等則把老的值替換成新的,然后返回舊值
        QEntry<K, V> e = table[index];
        while (e != null) {
            //先比較hash是否相等,再比較對象是否相等,或者比較equals方法
            //如果相等了,說明有一樣的key,這時要更新舊值為新的value,同時返回舊的值
            if (e.hash == hash && (key == e.key || key.equals(e.key))) {
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
            e = e.next;
        }

        //如果數組中沒有元素的key與傳的key相等的話
        //把當前位置的元素保存下來
        QEntry<K, V> next = table[index];

        //next有可能為null,也有可能不為null,不管是否為null
        //next都要作為新元素的下一個節點(next傳給了QEntry的構造函數)
        //然后新的元素保存在了index這個位置
        table[index] = new QEntry<>(key, value, hash, next);

        //如果需要擴容,元素的個數大於 table.length * 0.75 (別問為什么是0.75,經驗)
        if (size++ >= (table.length * DEFAULT_LOAD_FACTOR)) {
            resize();
        }

        return null;
    }

    //擴容,元素的個數大於 table.length * 0.75
    //數組擴容到原來大小的2倍
    private void resize() {
        //新建一個數組,大小為原來數組大小的2倍
        int newCapacity = table.length * 2;
        QEntry[] newTable = new QEntry[newCapacity];

        QEntry[] src = table;

        //遍歷舊數組,重新映射到新的數組中
        for (int j = 0; j < src.length; j++) {
            //獲取舊數組元素
            QEntry<K, V> e = src[j];

            //釋放舊數組
            src[j] = null;

            //因為e是一個鏈表,有可能有多個節點,循環遍歷進行映射
            while (e != null) {
                //把e的下一個節點保存下來
                QEntry<K, V> next = e.next;

                //e這個當前節點進行在新的數組中映射
                int i = indexFor(e.hash, newCapacity);

                //newTable[i] 位置上有可能是null,也有可能不為null
                //不管是否為null,都作為e這個節點的下一個節點
                e.next = newTable[i];

                //把e保存在新數組的 i 的位置
                newTable[i] = e;

                //繼續e的下一個節點的同樣的處理
                e = next;
            }
        }

        //所有的節點都映射到了新數組上,別忘了把新數組的賦值給table
        table = newTable;
    }

    //對hashCode進行運算,JDK中HashMap的實現,直接拷貝過來了
    static int hash(int h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    //根據 h 求key落在數組的哪個位置
    static int indexFor(int h, int length) {
        //或者  return h & (length-1) 性能更好
        //這里我們用最容易理解的方式,對length取余數,范圍就是[0,length - 1]
        //正好是table數組的所有的索引的范圍

        h = h > 0 ? h : -h; //防止負數

        return h % length;
    }

}

上面就是QHashMap的原理。下面我們寫一段測試代碼來看下我們的QHashMap能不能正常運行。測試代碼如下:

 public static void main(String[] args) {
        QHashMap<String, String> map = new QHashMap<>();
        map.put("name", "tom");
        map.put("age", "23");
        map.put("address", "beijing");
        String oldValue = map.put("address", "shanghai"); //key一樣,返回舊值,保存新值

        System.out.println(map.get("name"));
        System.out.println(map.get("age"));

        System.out.println("舊值=" + oldValue);
        System.out.println("新值=" + map.get("address"));
    }

輸出如下:

tom
23
舊值=beijing
新值=shanghai

通過上面的簡單的實現了QHashMap,還有好多功能沒有實現,比較remove,clear,containsKey()等,還有遍歷相關,有興趣的讀者可以自己實現


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM