帶你逐步實現簡易HashMap,助力理解Java HashMap

dawnchen發表於2019-03-13

一、實現簡易HashMap

作為講解用,不可用作生產環境!

第一步,把key實體轉化為一定範圍內的數字。

我們知道Java中所有的資料型別可以以物件的形式呈現的。

而且它為每個物件賦予了一個編號(即 hashCode()函式的返回值)

這樣我們就能使用某種方法將這個代表物件的編號轉化為一個一定範圍內的數字。(為了講解方便我們暫時把這個範圍設定為0~9)

private int hash(Object obj) {
    int h = obj.hashCode();
    // hashCode有可能是負數,如果是負數則取絕對值
    if(0 > h) {
        h *= -1;
    }
    return h%10
}
複製程式碼

第二步,設計資料的儲存方式

我們把物件編號轉化為了0~9中的一個數字,那麼必然會出現重複。

為了處理重複的值我們定義一個長度為10的陣列。每個陣列存的值是一條連結串列。

每當轉化後的數字重複時就在對應的連結串列後追加一個值。

帶你逐步實現簡易HashMap,助力理解Java HashMap
public class Node {
    // 物件
    public Object key;
    public Object value;
    // 下一節點
    public Node next;
}
Node store[] = new Node[10];
複製程式碼

第三步,實現設定功能 、 獲取功能 和 main函式

  1. 原始碼地址:github.com/dawnchengx/…
  2. 建立儲存節點類 Node.java
public class Node {
    // 物件
    public Object key;
    public Object value;
    // 下一節點
    public Node next;
    public Node(Object key, Object value, Node next) {
        this.key = key;
        this.value = value;
        this.next = next;
    }
}
複製程式碼
  1. 建立HashMap類 HashMap.java
public class HashMap {
    Node store[] = new Node[10];
    private int hash(Object obj) {
        int h = obj.hashCode();
        if(0 > h) {
            h *= -1;
        }
        return h%10;
    }
    // 設定方法
    public void put(Object key, Object value) {
        // 確定該物件放入陣列中的位置
        int position = this.hash(key);
        if (null == this.store[position]) {
            this.store[position] = new Node(key, value, null);
            return;
        }
        // 放入指定位置
        Node current = this.store[position];
        while(true) {
            if(current.key.equals(key)) {
                current.value = value;
                break;
            }
            if(null == current.next) {
                current.next = new Node(key, value, null);
                break;
            }
            current = current.next;
        }
    }
    // 獲取方法
    public Object get(Object key) {
        // 確定該物件放置的陣列位置
        int position = this.hash(key);
        if (null == this.store[position]) {
            return null;
        }
        Node current = this.store[position];
        while(true) {
            if(current.key.equals(key)) {
                return current.value;
            }
            if(null == current.next) {
                return null;
            }
            current = current.next;
        }
    }
}
複製程式碼
  1. 建立Main類 Main.java
public class Main {
    public static void main(String[] args) {
        HashMap myHM = new HashMap();
        // 測試基礎功能 
        myHM.put("hello", "world");
        myHM.put("foo", "bar");
        System.out.println(myHM.get("hello"));
        System.out.println(myHM.get("foo"));
        // 查詢不存在的資料 
        System.out.println(myHM.get("some"));

    }
}
複製程式碼
  1. 編譯執行Main.java
# 編譯執行Main.java
javac Main.java && java Main

# 輸出
world
bar
null
複製程式碼

二、擴容

由於HashMap會把物件的hashCode轉化為在一定範圍內數字,所以物件會過於集中於陣列中幾個位置。

這樣會導致連結串列變得非常長,減少檢索效率。為了在結構上提升效率,所以我們設定一個閥門值,比如陣列大小的0.75。當陣列內的值數達到陣列總長度的0.75時,自動觸發擴容操作。

  1. 原始碼地址:github.com/dawnchengx/…
  2. 修改HashMap類 HashMap.java
public class HashMap {
    private int defaultVal;
    private double scaleFactor;
    private Node store[];
    public HashMap() {
        this.defaultVal = 16;
        this.scaleFactor = 0.75;
        this.store = new Node[defaultVal];
    }
    public HashMap(int defaultVal, float scaleFactor) {
        this.defaultVal = defaultVal;
        this.scaleFactor = scaleFactor;
        this.store = new Node[defaultVal];
    }
    private int hash(Object obj) {
        int h = obj.hashCode();
        if(0 > h) {
            h *= -1;
        }
        return h%this.defaultVal;
    }
    // 設定方法
    public void put(Object key, Object value) {
        // 確定該物件放入陣列中的位置
        int position = this.hash(key);
        if (null == this.store[position]) {
            this.store[position] = new Node(key, value, null);
            return;
        }
        // 放入指定位置
        Node current = this.store[position];
        while(true) {
            if(current.key.equals(key)) {
                current.value = value;
                break;
            }
            if(null == current.next) {
                current.next = new Node(key, value, null);
                break;
            }
            current = current.next;
        }
        // 獲得陣列非空值
        int notNullNum = this.defaultVal;
        for(int i = 0; i < this.defaultVal; i++) {
            if (null == this.store[i]) {
                notNullNum--;
            }
        }
        // 達到閥門值,自動觸發擴容
        if ( notNullNum > (int)(this.defaultVal * this.scaleFactor) ) {
            this.resize(2);
        }
    }
    // 獲取方法
    public Object get(Object key) {
        // 確定該物件放置的陣列位置
        int position = this.hash(key);
        if (null == this.store[position]) {
            return null;
        }
        Node current = this.store[position];
        while(true) {
            if(current.key.equals(key)) {
                return current.value;
            }
            if(null == current.next) {
                return null;
            }
            current = current.next;
        }
    }
    // 擴容方法
    public void resize(int multiplier) {
        int oldDefaultVal = this.defaultVal;
        this.defaultVal *= multiplier;
        Node newStore[] = new Node[this.defaultVal];
        for(int i = 0; i < oldDefaultVal; i++) {
            if(null != this.store[i]) {
                newStore[this.hash(this.store[i].key)] = this.store[i];
            }
        }
        this.store= newStore;
    }
    // 列印擴容狀態
    public void testResize() {
        System.out.printf("當前陣列大小為%d\n", this.defaultVal);
        for(int i = 0; i < this.defaultVal; i++) {
            System.out.printf("第%d位:", i);
            Node current = this.store[i];
            int j = 0;
            while(true) {
                if(null != current) {
                    System.out.printf("%d->[%s]=%s ", j, current.key, current.value);
                }else {
                    System.out.printf("該位無值"); 
                    break;
                }
                if(null == current.next) {
                    break;
                }
                current = current.next;
                j++;
            }
            System.out.println();
        }
        System.out.println();
    }
}
複製程式碼
  1. 修改Main類 Main.java
public class Main {
    public static void main(String[] args) {
        HashMap myHM = new HashMap();
        // 測試基礎功能 
        myHM.put("hello", "world");
        myHM.put("foo", "bar");
        System.out.println(myHM.get("hello"));
        System.out.println(myHM.get("foo"));
        // 查詢不存在的資料 
        System.out.println(myHM.get("some"));

        // 新增足夠的值觸發第一次擴容
        for(int i = 0; i < 100; i++) {
            String key = "key"+i;
            String value = "value"+i;
            myHM.put(key, value);
        }
        // 列印擴容後的資料儲存情況
        myHM.testResize();
        // 新增足夠的值觸發第二次擴容
        for(int i = 100; i < 150; i++) {
            String key = "key"+i;
            String value = "value"+i;
            myHM.put(key, value);
        }
        // 列印擴容後的資料儲存情況
        myHM.testResize();
    }
}
複製程式碼

三、連結串列過長時轉化為紅黑樹

連結串列查詢、插入和刪除的漸近增長表示式為O(n),隨著連結串列的變長這些操作會越來越慢。

為了當連結串列過長時不影響增刪查的效率,當連結串列長度大於某個值是,我們把該連結串列轉化為紅黑樹。

由於TreeMap是由紅黑樹實現,我們就直接用TreeMap作為紅黑樹的結構使用。

  1. 原始碼:github.com/dawnchengx/…

  2. 修改HashMap類 HashMap.java

import java.util.TreeMap;
import java.util.Iterator;

public class HashMap {
    private int defaultVal;
    private double scaleFactor;
    private Node store[];
    private int toBRTreeVal = 4;
    public HashMap() {
        this.defaultVal = 16;
        this.scaleFactor = 0.75;
        this.store = new Node[defaultVal];
    }
    public HashMap(int defaultVal, float scaleFactor) {
        this.defaultVal = defaultVal;
        this.scaleFactor = scaleFactor;
        this.store = new Node[defaultVal];
    }
    private int hash(Object obj) {
        int h = obj.hashCode();
        if(0 > h) {
            h *= -1;
        }
        return h%this.defaultVal;
    }
    // 設定方法
    public void put(Object key, Object value) {
        // 確定該物件放入陣列中的位置
        int position = this.hash(key);
        if (null == this.store[position]) {
            this.store[position] = new Node(key, value, null);
            return;
        }
        // 放入指定位置
        Node current = this.store[position];
        int linkLen = 0;
        while(true) {
            linkLen++;
            if(current.key.equals(key)) {
                current.value = value;
                break;
            }
            if(null == current.next) {
                current.next = new Node(key, value, null);
                break;
            }
            current = current.next;
        }
        if (this.toBRTreeVal < linkLen) {
            TreeMap treeMap = new TreeMap();
            while(true) {
                treeMap.put(current.key, current.value);
                if(null == current.next) {
                    break;
                }
                current = current.next;
            }
            this.store[position].key = "BRTree";
            this.store[position].value = treeMap;
        }
        // 獲得陣列非空值
        int notNullNum = this.defaultVal;
        for(int i = 0; i < this.defaultVal; i++) {
            if (null == this.store[i]) {
                notNullNum--;
            }
        }
        // 達到閥門值,自動觸發擴容
        if ( notNullNum > (int)(this.defaultVal * this.scaleFactor) ) {
            this.resize(2);
        }
    }
    // 獲取方法
    public Object get(Object key) {
        // 確定該物件放置的陣列位置
        int position = this.hash(key);
        if (null == this.store[position]) {
            return null;
        }
        Node current = this.store[position];
        while(true) {
            if("RBTree" == current.key && current.value instanceof TreeMap) {
                TreeMap currentTreeMap = (TreeMap)current.value;
                return currentTreeMap.get(key);
            }
            if(current.key.equals(key)) {
                return current.value;
            }
            if(null == current.next) {
                return null;
            }
            current = current.next;
        }
        
    }
    // 擴容方法
    public void resize(int multiplier) {
        int oldDefaultVal = this.defaultVal;
        this.defaultVal *= multiplier;
        Node newStore[] = new Node[this.defaultVal];
        for(int i = 0; i < oldDefaultVal; i++) {
            if(null != this.store[i]) {
                Node current = this.store[i];
                if (current.key == "RBTree" && current.value instanceof TreeMap) {
                    TreeMap currentTreeMap = (TreeMap)current.value;
                    newStore[this.hash(currentTreeMap.firstKey())] = current;
                } else if (current instanceof Node) {
                    newStore[this.hash(current.key)] = current;
                }
            }
        }
        this.store= newStore;
    }
    // 列印轉化狀況
    public void testTrans() {
        System.out.printf("當前陣列大小為%d\n", this.defaultVal);
        for(int i = 0; i < this.defaultVal; i++) {
            System.out.printf("第%d位:", i);
            Node current = this.store[i];
            if (null != current && current.key == "RBTree" && current.value instanceof TreeMap) {
                TreeMap currentTreeMap = (TreeMap)current.value;
                Iterator it = currentTreeMap.keySet().iterator();
                while (it.hasNext()) {
                    System.out.printf("BRTree:[%s]=%s", it.next(), currentTreeMap.get(it.next()));
                }
            } else if (current instanceof Node) {
                int j = 0;
                while(true) {
                    if(null != current) {
                        System.out.printf("link:%d->[%s]=%s ", j, current.key, current.value);
                    }else {
                        System.out.printf("該位無值");
                        break;
                    }
                    if(null == current.next) {
                        break;
                    }
                    current = current.next;
                    j++;
                }
            } else {
                System.out.printf("無");
            }
            System.out.println();
        }
        System.out.println();
    }
}
複製程式碼
  1. 修改Main類 Main.java
public class Main {
    public static void main(String[] args) {
        HashMap myHM = new HashMap();
        // 測試基礎功能 
        myHM.put("hello", "world");
        myHM.put("foo", "bar");
        System.out.println(myHM.get("hello"));
        System.out.println(myHM.get("foo"));
        // 查詢不存在的資料 
        System.out.println(myHM.get("some"));

        // 新增足量值觸發紅黑樹
        for(int i = 0; i < 300; i++) {
            String key = "key"+i;
            String value = "value"+i;
            myHM.put(key, value);
        }
        // 列印轉換情況
        myHM.testTrans();
    }
}
複製程式碼

四、總結

本文通過簡易的程式碼闡述了Java類庫HashMap的基礎原理,如果想要更深一步學習請移步https://github.com/Snailclimb/JavaGuide/blob/master/Java/HashMap.md。

並參照原始碼學習。

相關文章