JDK源碼閱讀,手寫HashMap

HashMap是jdk提供的最常用的容器之一,jdk 1.7及之前版本,HashMap底層基於數組和單鏈表結構,數組每個元素是一對鍵值對對象,該對象包括hash值,key,value以及單鏈表下一個鍵值對的引用。jdk 1.8對HashMap底層結構做了一些改進,當數組同一位置的鍵值對超過8個,不再以單鏈表形式存儲,而是改爲紅黑樹。進一步提升了性能。

本文基於jdk 1.7,參考源碼,手寫一個簡單的HashMap,實現自動擴容功能、put、get、entrySet方法。
HashMap結構如下圖:
在這裏插入圖片描述

1、HashMap的一些性質

通過閱讀源碼,相較於HashTable,HashMap有以下性質:

  • 線程安全:Hashtable線程安全,同步,同步方式是鎖住整個Hashtable,效率相對低下
    HashMap線程不安全,非同步,效率相對高
  • 父類:Hashtable是Dictionary
    HashMap是AbstractMap
  • null值:Hashtable鍵與值不能爲null
    HashMap鍵最多一個null,值可以多個null
  • 初始容量:HashMap的初始容量爲16,
    Hashtable初始容量爲11
  • 擴容方式:HashMap擴容時是當前容量翻倍即:capacity2,
    Hashtable擴容時是容量翻倍+1即:capacity
    2+1。
  • 計算hash的方法:HashMap計算hash對key的hashcode進行了二次hash,以獲得更好的散列值,然後對table數組長度取摸。
    Hashtable計算hash是直接使用key的hashcode對table數組的長度直接進行取模
  • 存儲結構:jdk1.8以後,HashMap在鏈表長度超過閾值時,改用數組+紅黑樹的方式存儲
    Hashtable採用數組+鏈表方式存儲

這些性質都體現在源碼中,通過代碼,可以有更深刻的認識。

2、手寫HashMap

這裏還是用常規命名MyHashMap作爲我們的HashMap的類名

定義接口 MyMap

public interface MyMap<K, V> {
    V put(K k,V v);
    V get(K k);
    Set<? extends Entry<K, V>> entrySet();
    interface Entry<K, V>{
        K getKey();
        V getValue();
    }
}

這裏定義了後面即將去實現的三個方法,和一個內部接口類型。

MyhashMap的一些屬性

public class MyHashMap<K,V> implements MyMap<K,V> {
    //默認初始化的容量,16
    private static final int DEFAULT_INITIAL_CAPACITY = 1<<4;
    //默認初始化的擴容因子
    private static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //容量
    private int initialCapacity;
    //擴容因子
    private float loadFactor;
    //Entry數量,也就是map的長度
    int size;
    //entry數組
    private Node<K,V>[] table;

有兩個常量,一個初始化容量16,一個擴容因子0.75,都與jdk源碼保持一致。兩個變量initialCapacity,loadFactor就是對應的兩個屬性,size是MyHashMap鍵值對個數,table是存放鍵值對的數組。
前面說了HashMap是基於數組+鏈表形式存儲鍵值對,那麼鏈表數據結構就在下面的靜態內部類Node的結構中體現

靜態內部類 Node

static class Node<K,V> implements MyMap.Entry<K,V>{
        K key;
        V value;
        Node<K,V> next;

        public Node() {
        }

        Node(K key, V value, Node<K, V> next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }

        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return value;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Node<?, ?> node = (Node<?, ?>) o;
            return Objects.equals(key, node.key) &&
                    Objects.equals(value, node.value);
        }

        @Override
        public int hashCode() {
            return Objects.hash(key, value);
        }

        @Override
        public String toString() {
            return key+"="+value;
        }
    }

Node類實現前面定義的MyMap.Entry接口,有三個屬性,key、value以及單鏈表下一個節點的引用,實際上jdk源碼還有一個hash屬性存儲hash值,這裏簡化掉。Node的兩個方法getKey,getValue非常簡單,獲取鍵和值,源碼有個setValue方法,這裏也簡化掉,:)然後就是重寫hashCode方法和equals方法,使得判斷兩個Node相等的依據是key值和value值都相等。

MyHashMap的構造方法

	public MyHashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity<0) {
            throw new IllegalArgumentException("Illegal initial capacity: "+initialCapacity);
        }
        if (loadFactor <= 0 || Float.isNaN(loadFactor)){
            throw new IllegalArgumentException("Illegal load factor: " +loadFactor);
        }
        this.loadFactor = loadFactor;
        this.initialCapacity = initialCapacity;
        this.table = new Node[this.initialCapacity];
    }

    public MyHashMap() {
        this(DEFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);
    }

用戶可以構造自定義初始容量和擴容因子的MyHashMap,如果自定義的數據不合法,拋出運行時異常。使用空構造是使用默認的16和0.75作爲初始容量和擴容因子。非常簡單。

put方法

接下來看put方法

	@Override
    public V put(K key, V value) {
        V oldValue = null;
        //是否需要擴容
        if (size>=initialCapacity*loadFactor){
            //數組容量擴大爲兩倍
            expand(2*initialCapacity);
        }
        //根據key的hash值確定應該放入的數組位置
        int index = hash(key)&(initialCapacity-1);
        if (table[index]==null){
            table[index] = new Node<K,V>(key,value,null);
        }else{//遍歷單鏈表
            Node<K,V> node = table[index];
            Node<K,V> e = node;
            while(e!=null){
                if (e.key==key||e.key.equals(key)){
                    oldValue = e.value;
                    e.value=value;
                    return oldValue;
                }
                e = e.next;
            }
            table[index] = new Node<K,V>(key,value,node);
        }
        ++size;
        return oldValue;
    }

插入node之前先檢查鍵值對數量是否大於容量*擴容因子,若超過則需先擴容。擴容方法和hash方法後面再看。
put方法返回值爲map中對應key原來的value值,若存在該key,更新其value值,並返回舊值;
若不存在該key,則通過hash取模將鍵值對插入到數組對應index的位置,若該位置有其他node,將要插入的鍵值對插入到單鏈表頭部。size加1.

get方法

獲取對應key值的鍵值對的value值。

	@Override
    public V get(K key) {
        int index = hash(key)&(initialCapacity-1);
        Node<K,V> e = table[index];
        while(e!=null){
            if (e.key==key||e.key.equals(key)){
                return e.value;
            }
            e=e.next;
        }
        return null;
    }

根據key的hash得到數組index,遍歷該位置的單鏈表獲取鍵值對。

entrySet方法

	@Override
    public Set<Node<K, V>> entrySet() {
        Set<Node<K,V>> set = new HashSet<>();
        for (Node<K,V> node:table){
            while(node!=null){
                set.add(node);
                node = node.next;
            }
        }
        return set;
    }

遍歷數組和鏈表,返回所有鍵值對的集合。

擴容方法 expand

	//擴容方法,將舊數組的數據取出來通過put方法放進新數組
    private void expand(int i) {
        Node<K,V> [] newTable = new Node[i];
        initialCapacity = i;
        size = 0;
        Set<Node<K, V>> set = entrySet();
        //替換數組引用
        if (newTable.length>0){
            table = newTable;
        }
        for (Node<K,V> node:set){
            put(node.key,node.value);
        }
    }

源碼裏該方法叫resize,通過entrySet方法獲取所有鍵值對的集合,put進新的數組裏面。

hash方法

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

參考源碼的hash方法。
以上就是完整的MyHashMap類。實現HashMap的基本功能。
測試類:

public class TestMain {
    public static void main(String[] args) {
        MyHashMap<String, String> myHashMap = new MyHashMap<>();
        //put()
        for (int i = 1; i <=500 ; i++) {
            myHashMap.put("KEY_"+i,"VALUE_"+i);
        }
        //size
        System.out.println("【SIZE】-->"+myHashMap.size);
        //get()
        System.out.println(myHashMap.get("KEY_444"));
        //entrySet()
        for (MyHashMap.Node<String, String> entry : myHashMap.entrySet()) {
            if(entry.getKey().equals("KEY_333"))
            System.out.println(entry);
        }
    }
}

運行結果:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章