理解Java中HashMap的工做原理

Java中的HashMap使用散列來高效的查找和存儲值。HashMap內部使用Map.Entry的形式來保存key和value,使用put(key,value)方法存儲值,使用get(key)方法查找值。編程

理解hashCode()

Java中的hashCode()方法,是頂層對象Object中的方法,所以Java中全部的對象都會帶有hashCode()方法。
在各類最佳實踐中,都會建議在編寫本身的類的時候要同時覆蓋hashCode()equals()方法,可是在使用散列的數據結構時(HashMap,HashSet,LinkedHashSet,LinkedHashMap),
若是不爲鍵覆蓋hashCode()equals()方法,將沒法正確的處理該鍵。api

hashCode()方法返回一個int值,這個int值就是用這個對象的hashCode()方法產生的hash值。數組

HashMap的工做原理

在散列表中查找一個值的過程爲,先經過鍵的hashCode()方法計算hash值,而後使用hash值產生下標並使用下標查找數組,這裏爲何要用數組呢,由於數組是存儲一組元素最快的數據結構,所以使用數組來表示鍵的信息。數據結構

因爲數組的容量(也就是表中的桶位數)是固定的,因此不一樣的鍵能夠產生相同的下標,也就是說,可能會有衝突,所以數組多大就不重要了,任何鍵總能在數組中找到它的位置。ide

數組並不直接保存值,由於不一樣的鍵可能產生相同的數組下標,數組保存的是LinkedList,所以,散列表的存儲結構外層是一個數組,容量固定,數組的每一項都是保存着Entry Object(同時保存key和value)的LinkedList。函數

因爲下標的衝突,不一樣的鍵可能會產生相同的bucket location,在使用put(key,value)時,若是兩個鍵產生了相同的bucket location,因爲LinkedList的長度是可變的,因此會在該LinkedList中再增長一項Entry Object,其中保存着key和value。code

鍵使用hashCode()方法產生hash值後,利用hash值產生數組的下標,找到值在散列表中的桶位(bucket),也就是在哪個LinkedList中,若是該桶位只有一個的Object,則返回該Value,若是該桶位有多個Object,那麼再對該LinkedList中的Entry Object的鍵使用equals()方法進行線性的查詢,最後找到該鍵的值並返回。對象

最後對LinkedList進行線性查詢的部分會比較慢,可是,若是散列函數好的話,數組的每一個位置就只有較少的值,所以不是查詢整個LinkedList,而是快速地跳到數組的某個位置,只對不多的元素進行比較,這就是HashMap會如此快的緣由。遞歸

在知道了散列的原理後咱們能夠本身實現一個簡單的HashMap(例子來源於《Java編程思想(第四版)》)get

public class SimpleHashMap<K, V> extends AbstractMap<K, V> {
    //內部數組的容量
    static final int SIZE = 997;

    //buckets數組,內部是一個鏈表,鏈表的每一項是Map.Entry形式,保存着HashMap的值
    @SuppressWarnings("unchecked")
    LinkedList<MapEntry<K, V>>[] buckets = new LinkedList[SIZE];

    public V put(K key, V value) {
        V oldValue = null;
        //使用hashCode()方法產生hash值,使用hash值與數組容量取餘得到數組的下標
        int index = Math.abs(key.hashCode()) % SIZE;
        //若是該桶位爲null,則插入一個鏈表
        if (buckets[index] == null) {
            buckets[index] = new LinkedList<>();
        }
        //得到bucket
        LinkedList<MapEntry<K, V>> bucket = buckets[index];
               
        MapEntry<K, V> pair = new MapEntry<>(key, value);
        boolean found = false;
        
        ListIterator<MapEntry<K, V>> it = bucket.listIterator();
        while (it.hasNext()) {
            MapEntry<K, V> iPair = it.next();
            //對鍵使用equals()方法線性查詢value
            if (iPair.getKey().equals(key)) {
                oldValue = iPair.getValue();
                //找到了鍵之後更改鍵原來的value
                it.set(pair);
                found = true;
                break;
            }
        }
        //若是沒找到鍵,在bucket中增長一個Entry
        if (!found) {
            buckets[index].add(pair);
        }
        return oldValue;
    }
    
    //get()與put()的工做方式相似
    @Override
    public V get(Object key) {
        //使用hashCode()方法產生hash值,使用hash值與數組容量取餘得到數組的下標
        int index = Math.abs(key.hashCode()) % SIZE;
        if (buckets[index] == null) {
            return null;
        }
        //使用equals()方法線性查找鍵
        for (MapEntry<K, V> iPair : buckets[index]) {
            if (iPair.getKey().equals(key)) {
                return iPair.getValue();
            }
        }
        return null;
    }

    @Override
    public Set<Map.Entry<K, V>> entrySet() {
        Set<Map.Entry<K, V>> set = new HashSet<>();
        for (LinkedList<MapEntry<K, V>> bucket : buckets) {
            if (bucket == null) {
                continue;
            }
            for (MapEntry<K, V> mpair : bucket) {
                set.add(mpair);
            }
        }
        return set;
    }

    public static void main(String[] args) {
        SimpleHashMap<String, String> m = new SimpleHashMap<>();
        m.putAll(Countries.capitals(25));
        System.out.println(m);
        System.out.println(m.get("ERITREA"));
        System.out.println(m.entrySet());
    }
}

編寫良好的hashCode()方法

若是hashCode()產生的hash值可以讓HashMap中的元素均勻分佈在數組中,能夠提升HashMap的運行效率。
一個良好的hashCode()方法首先是能快速地生成hash值,而後生成的hash值能使HashMap中的元素在數組中儘可能均勻的分佈,
hash值不必定是惟一的,由於容量是固定的,總會有下標衝突的狀況產生。

《Effective Java》中給出了覆蓋hashCode()方法的最佳實踐:

  1. 把某個非零的常數值,好比17,保存在一個名爲result的int類型中。

  2. 對於對象中的每一個關鍵域f(指equals()方法中涉及的域),完成如下步驟:

    • 爲該域計算int類型的散列碼c,根據域的類型的不一樣,又能夠分爲如下幾種狀況:

      • 若是該域是boolean類型,則計算(f?1:0)

      • 若是該域是String類型,則使用該域的hashCode()方法

      • 若是該域是byte、char、short或int類型,則計算(int)f

      • 若是該域是long類型,則計算(int)(f^>>>32)

      • 若是該域是float類型,則計算Float.floatToIntBits(f)

      • 若是該域是double類型,則計算Double.doubleToLongBits(f)返回一個long類型的值,再根據long類型的域,生成int類型的散列碼

      • 若是該域是一個對象引用,而且該類的equals()方法經過遞歸調用equals方式來比較這個域,則一樣爲這個域遞歸地調用hashCode()

      • 若是該域是一個數組,則要把每個元素看成單獨的域來處理,也就是說遞歸地應用上述原則

  3. 按照公式:result = 31 * result + c,返回result。

寫一個簡單的類並用上述的規則來覆蓋hashCode()方法

public class SimpleHashCode {
    private static long counter = 0;
    private final long id = counter++;
    private String name;
    
    @Override
    public int hashCode(){
        int result = 17;
        if (name != null){
            result = 31 * result + name.hashCode(); 
        }
        result = result * 31 + (int) id;
        return result;
    }
    
    @Override
    public boolean equals(Object o){
        return o instanceof SimpleHashCode && id == ((SimpleHashCode)o).id;
    }
}
相關文章
相關標籤/搜索