Java容器—— 「實現自己的HashMap」

一、前言

上一篇文章實現了自己的ArrayMap,但是對於Key-Value使用單純數組進行存儲,那麼性能實在是慘不忍睹。此種情況下哈希表的數據結構是比較合適的解決方案。

哈希表就是一種以 鍵-值(key-indexed) 存儲數據的結構,我們只要輸入待查找的值即key,即可查找到其對應的值。
哈希的思路很簡單,如果所有的鍵都是整數,那麼就可以使用一個簡單的無序數組來實現:將鍵作爲索引,值即爲其對應的值,這樣就可以快速訪問任意鍵的值。這是對於簡單的鍵的情況,我們將其擴展到可以處理更加複雜的類型的鍵。

使用哈希查找有兩個步驟:

1. 使用哈希函數將被查找的鍵轉換爲數組的索引。
在理想的情況下,不同的鍵會被轉換爲不同的索引值,但是在有些情況下我們需要處理多個鍵被哈希到同一個索引值的情況。所以哈希查找的第二個步驟就是處理衝突

2. 處理哈希碰撞衝突。
有很多處理哈希碰撞衝突的方法,如拉鍊法和線性探測法。HashMap中使用單向鏈表來解決衝突問題。
哈希表是一個在時間和空間上做出權衡的經典例子。如果沒有內存限制,那麼可以直接將鍵作爲數組的索引。那麼所有的查找時間複雜度爲O(1);如果沒有時間限制,那麼我們可以使用無序數組並進行順序查找,這樣只需要很少的內存。哈希表使用了適度的時間和空間來在這兩個極端之間找到了平衡。只需要調整哈希函數算法即可在時間和空間上做出取捨。

本文嘗試按照哈希表的設計思路,設計一個SelfHashMap。(文章編寫順序是按照對哈希表的理解遞進,目的爲了更好的掌握jdk的HashMap)

二、設計思路

1.hash算法:對於原值不確定的情況下,使用取餘法最爲簡單,因此就確定使用除留餘數法,將餘數作爲數組的索引。

假設哈希表長爲m,p爲小於等於m的最大素數,則哈希函數爲
h(k)=k % p ,其中%爲模p取餘運算

對於SelfHashMap而言,鍵不一定爲整數,因此需要獲取鍵的HashCode然後再除留取餘得到數組下標。同時由於hashCode可能爲負數,取其絕對值。
2.衝突解決方法:對於不確定的長度的HashMap,使用拉鍊法更有優勢,因此確定使用單向鏈表的方式。
3.數組擴容(resize):當Map中的元素很多的時候,必然會出現很多Hash衝突的情況(很多h(k)都定位到數組的相同位置),這時查詢和存儲的效率就開始大幅度的下降,因此就要進行擴容和再哈希。
不過何時進行擴容這是一個值得思考的問題。擴容的太晚意味着在擴容之前的查詢效率很低,擴容的太早則意味着存儲空間的浪費。此處設置一個影響因子(Hashmap中的loadFactor),當map中元素的數量達到loadFactor*數組長度時進行擴容。
4.null對象的處理:直接把null對象放在數組[0]的位置。(如果拒絕則更加簡單)

三、代碼實現

1.鏈表實現

class Node<K, V> implements Entry<K, V> {
       private K key;
       private V value;
       /**
        * 鏈表的下一個節點
        */
       private Node<K, V> next;
       /**
        * key的hash值,考慮到如果再hash就不用重新算一遍hash值
        */
       private int hashCode;

       public Node(K key, V value, int hashCode, Node<K, V> next) {
           this.key = key;
           this.value = value;
           this.hashCode = hashCode;
           this.next = next;
       }
       @Override
       public K getKey() {
           return key;
       }
       @Override
       public V getValue() {
           return value;
       }
       @Override
       public V setValue(V value) {
           this.value = value;
           return value;
       }
       @Override
       public String toString() {
           return key + "=" + value;
       }
    }

2.Hash算法

int hashCode = hash(key);
int index = indexForArray(hashCode, arrayLength);

/**
 * 根據hash值以及數組長度獲取數組下表
 * 之後更改Hash算法可以直接更改此處
 *
 * @param hashCode
 * @param arrayLength
 * @return
 */
private int indexForArray(int hashCode, int arrayLength) {
    int index = Math.abs(hashCode) % arrayLength;
    return index;
}

/**
 * 獲取hash值
 *
 * @param key 傳入的鍵
 * @return key爲null返回0,其他返回hashCode()的值
 */
public int hash(Object key) {
    return key == null ? 0 : key.hashCode();
}

3.數組擴容

這裏寫圖片描述
這裏寫圖片描述
這裏寫圖片描述

/**
 * 擴容
 * 1.得到新數組長度,創建新數組
 * 2.將舊數組的數據轉移到新數組
 * 3.替換舊數組
 */
private void resize() {
    int newLength = arrayLength * 2;
    Node<K, V>[] newTables = new Node[newLength];
    Set<Entry<K, V>> entrySet = entrySet();
    int newSize = 0;
    for (Entry entry : entrySet) {
        Node<K, V> node = (Node<K, V>) entry;
        node.next = null;
        int index = indexForArray(node.hashCode, arrayLength);
        Node<K, V> indexNode = newTables[index];
        if (indexNode == null) {
            newTables[index] = node;
        } else {
            while (indexNode.next != null) {
                indexNode = indexNode.next;
            }
            indexNode.next = node;
        }
    }
    tables = newTables;
    arrayLength = newLength;
}

完整代碼如下:

/**
 * @author lzy
 * @date 2018/1/18
 */
public class SelfHashMap<K, V> extends AbstractMap<K, V> implements Map<K, V>, Cloneable, Serializable {

    /**
     * 默認的負載因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    /**
     * 初始默認長度
     */
    private static final int DEFAULT_LENGTH = 16;
    /**
     * tables的長度
     */
    private int arrayLength;

    /**
     * map中的元素數量
     */
    private int size;

    private float loadFactor;

    /**
     * 存儲節點的數組
     */
    private Node<K, V>[] tables;

    public SelfHashMap() {
        this(DEFAULT_LENGTH, DEFAULT_LOAD_FACTOR);
    }

    /**
     * @param length 數組初始化長度
     */
    public SelfHashMap(int length) {
        this(length, DEFAULT_LOAD_FACTOR);
    }

    /**
     * @param length     數組初始化長度
     * @param loadFactor 負載因子
     */
    public SelfHashMap(int length, float loadFactor) {
        if (length <= 0) {
            throw new IllegalArgumentException("初始化長度必須大於0");
        }
        if (loadFactor <= 0) {
            throw new IllegalArgumentException("負載因子必須大於0");
        }

        this.arrayLength = length;
        this.loadFactor = loadFactor;
        tables = new Node[length];
    }

    @Override
    public V get(Object key) {
        int index = indexForArray(hash(key), arrayLength);
        Node<K, V> node = tables[index];
        for (Node<K, V> n = node; n != null; n = n.next) {
            if ((key == null && null == n.getKey()) || (key != null && key.equals(n.getKey()))) {
                return n.value;
            }
        }
        return null;
    }

    /**
     * @param key   鍵
     * @param value 值
     * @return 替換的舊value,或者null
     */
    @Override
    public V put(K key, V value) {
        int hashCode = hash(key);
        int index = indexForArray(hashCode, arrayLength);
        //如果當前位置爲
        Node<K, V> node = tables[index];
        if (node == null) {
            tables[index] = new Node(key, value, hashCode, null);
        } else {
            for (Node<K, V> n = node; n != null; n = n.next) {
                // 如果該key已經存在,則覆蓋並且返回
                K nodeKey = n.getKey();
                if ((key == null && null == nodeKey) || (key != null && key.equals(nodeKey))) {
                    V oldValue = n.getValue();
                    n.setValue(value);
                    return oldValue;
                }
                // 不存在該Key,判斷到隊列最後一個,則新建一個Node放在隊列尾部
                if (n.next == null) {
                    n.next = new Node<>(key, value, hashCode, null);
                    break;
                }
            }
        }
        //判斷是否要擴容,如果只是替換value,不增加元素,則不會執行到此處
        if (++size > arrayLength * loadFactor) {
            resize();
        }
        return null;
    }

    @Override
    public void clear() {
        tables = new Node[arrayLength];
        size = 0;
    }

    @Override
    public int size() {
        return size;
    }

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

    /**
     * 獲取hash值
     *
     * @param key 傳入的鍵
     * @return key爲null返回0,其他返回hashCode()的值
     */
    public int hash(Object key) {
        return key == null ? 0 : key.hashCode();
    }

    /**
     * 根據hash值以及數組長度獲取數組下表
     * 之後更改Hash算法可以直接更改此處
     *
     * @param hashCode
     * @param arrayLength
     * @return
     */
    private int indexForArray(int hashCode, int arrayLength) {
        int index = Math.abs(hashCode) % arrayLength;
        return index;
    }

    /**
     * 擴容
     * 1.得到新數組長度,創建新數組
     * 2.將舊數組的數據轉移到新數組
     * 3.替換舊數組
     */
    private void resize() {
        int newLength = arrayLength * 2;
        Node<K, V>[] newTables = new Node[newLength];
        Set<Entry<K, V>> entrySet = entrySet();
        int newSize = 0;
        for (Entry entry : entrySet) {
            Node<K, V> node = (Node<K, V>) entry;
            node.next = null;
            int index = indexForArray(node.hashCode, arrayLength);
            Node<K, V> indexNode = newTables[index];
            if (indexNode == null) {
                newTables[index] = node;
            } else {
                while (indexNode.next != null) {
                    indexNode = indexNode.next;
                }
                indexNode.next = node;
            }
        }
        tables = newTables;
        arrayLength = newLength;
    }

    class Node<K, V> implements Entry<K, V> {
        private K key;
        private V value;
        /**
         * 鏈表的下一個節點
         */
        private Node<K, V> next;
        /**
         * key的hash值
         */
        private int hashCode;

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

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

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

        @Override
        public V setValue(V value) {
            this.value = value;
            return value;
        }

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

    }
}

四、JdK1.7 HashMap分析

通過源碼可以看到,1.7中HashMap實現的大致思路是一致的,一些細節設計上比SelfHashMap充分點。1.8中邏輯由於加入紅黑樹,存在鏈表和紅黑樹之間的轉換。
總的而言,差異在以下幾點:(源碼只列HashMap)

1、數組初始化差異

HashMap:中的數組在第一次放入元素時才進行初始化,節省內容空間。
SelfHashMap:在一開始就進行初始化

public V put(K key, V value) {
    //如果table數組爲空數組{},進行數組填充(爲table分配實際內存空間),入參爲threshold,此時threshold爲initialCapacity 默認是1<<4(=16)
     if (table == EMPTY_TABLE) {
         inflateTable(threshold);//分配數組空間
     }
    // 下面邏輯省略
}
private void inflateTable(int toSize) {
    int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次冪
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此處爲threshold賦值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不會超過MAXIMUM_CAPACITY,除非loadFactor大於1
    table = new Entry[capacity];//分配空間
    initHashSeedAsNeeded(capacity);//選擇合適的Hash因子
}

2、擴容判斷依據差異

HashMap:存在一個成員變量threshold(閾值),作爲數組擴容的判斷依據。在元素大於等於閾值&數組當前位置不爲空(即又需要下掛一個鏈表節點),擴容爲當前的兩倍。先擴容再放入新的節點。
SelfHashMap:未使用threshold(閾值),通過數組的長度*loadFactor進行判斷擴容。先加入新的節點,然後判斷元素數量>數組的長度*loadFactor後進行擴容(resize)。

public V put(K key, V value) {
    // 當判斷出需要存入該key時,調用addEntry    
    addEntry(hash, key, value, i);//新增一個entry
    return null;
}

void addEntry(int hash, K key, V value, int bucketIndex) {
     if ((size >= threshold) && (null != table[bucketIndex])) {
         resize(2 * table.length);//當size超過臨界閾值threshold,並且即將發生哈希衝突時進行擴容,新容量爲舊容量的2倍
         hash = (null != key) ? hash(key) : 0;
         bucketIndex = indexFor(hash, table.length);//擴容後重新計算插入的位置下標
     }

     //把元素放入HashMap的桶的對應位置
     createEntry(hash, key, value, bucketIndex);
 }
//創建元素  
 void createEntry(int hash, K key, V value, int bucketIndex) {  
     Entry<K,V> e = table[bucketIndex];  //獲取待插入位置元素
     table[bucketIndex] = new Entry<>(hash, key, value, e);//這裏執行鏈接操作,使得新插入的元素指向原有元素。
//這保證了新插入的元素總是在鏈表的頭  
     size++;//元素個數+1  
 }

3、HashMap容量差異

HashMap數組容量需要爲2的冪次,同時最大容量爲1 << 30(2的30次方)。SelfHashMap和HashTable一樣沒有限制。

// 在inflateTable(int toSize)中調用
private static int roundUpToPowerOf2(int number) {
    // assert number >= 0 : "number must be non-negative";
    return number >= MAXIMUM_CAPACITY
            ? MAXIMUM_CAPACITY
            : (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}

4、Null處理方式差異

HashMap: put中單獨做判斷處理
SelfHashMap:統一處理。

public V put(K key, V value) {
    ...
   //如果key爲null,存儲位置爲table[0]或table[0]的衝突鏈上
    if (key == null)
        return putForNullKey(value);
    ...
}
// 通過遍歷table[0],然後替換key==null的Node的value。單獨處理確實較少很多額外的判斷 
private V putForNullKey(V value) {
   for (Entry<K,V> e = table[0]; e != null; e = e.next) {
       if (e.key == null) {
           V oldValue = e.value;
           e.value = value;
           e.recordAccess(this);
           return oldValue;
       }
   }
   modCount++;
   addEntry(0, null, value, 0);
   return null;
}

5、鏈表數據存放差異

HashMap:在放入一個新值的時候,放在鏈表的頭部,即數組的位置。放在頭部邏輯上非常簡單,效率更高。
SelfHashMap: 在放在鏈表的尾部。

//創建元素  
void createEntry(int hash, K key, V value, int bucketIndex) {  
    Entry<K,V> e = table[bucketIndex];  //獲取待插入位置元素
    table[bucketIndex] = new Entry<>(hash, key, value, e);//這裏執行鏈接操作,使得新插入的元素指向原有元素。
//這保證了新插入的元素總是在鏈表的頭  
    size++;//元素個數+1  
}  

6、Hash算法處理差異

HashMap對key的hashcode進一步進行計算以及二進制位的調整等來保證最終獲取的存儲位置儘量分佈均勻

//HashMap源碼:用了很多的異或,移位等運算
final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {//這裏針對String優化了Hash函數,是否使用新的Hash函數和Hash因子有關  
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

7、位運算提高效率

HashMap:使用位運算來提高效率。例如下面代碼等價與int index = h%length

//HashMap源碼
//返回數組下標
static int indexFor(int h, int length) {
    return h & (length-1);
}

h&(length-1)保證獲取的index一定在數組範圍內,舉個例子,默認容量16,length-1=15,h=18,轉換成二進制計算爲:
這裏寫圖片描述
最終計算出的index=2。此處的計算功能上等價於取模運算,不過位運算對計算機來說,性能更高一些。

Jdk HashMap的設計值得學習的地方很多,通過自己設計編寫一個哈希表,加深了對源碼的理解,也領會其設計之精妙。

五、性能測試

1.Get測試

A:ArrayMap
L:jdk1.7 LinkedHashMap
H:jdk1.7 HashMap
S:SelfHashMap

測試次數 A(1k) L(1k) H(1k) S(1K) A(1w) L(1w) H(1w) S(1w) A(10w) L(10w) H(10w) S(10w)
1k 14 1 1 1 7 1 4 1 97 1 1 1
1w 7 2 1 5 80 1 1 1 919 1 1 1
10w 81 4 4 4 1090 4 4 4 9755 9 7 12

2.Put測試

A:ArrayMap
L:jdk1.7 LinkedHashMap
H:jdk1.7 HashMap
S:SelfHashMap

測試次數 A(1k) L(1k) H(1k) S(1K) A(1w) L(1w) H(1w) S(1w) A(10w) L(10w) H(10w) S(10w)
1k <1 <1 <1 <1 8 <1 <1 <1 105 <1 <1 <1
1w 35 1 1 1 93 1 2 1 1068 2 1 1
10w 6999 8 16 36 10211 9 9 31 18631 9 11 12
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章