1. 程式人生 > >理解Java中HashMap的工作原理

理解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()方法,將無法正確的處理該鍵。

hashCode()方法返回一個int值,這個int值就是用這個物件的hashCode()方法產生的hash值。

HashMap的工作原理

在散列表中查詢一個值的過程為,先通過鍵的hashCode()方法計算hash值,然後使用hash值產生下標並使用下標查詢陣列, 這裡為什麼要用陣列呢,因為陣列是儲存一組元素最快的資料結構,因此使用陣列來表示鍵的資訊。

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

陣列並不直接儲存值,因為不同的鍵可能產生相同的陣列下標,陣列儲存的是LinkedList,因此, 散列表的儲存結構外層是一個數組,容量固定,陣列的每一項都是儲存著Entry Object(同時儲存key和value)的LinkedList。

由於下標的衝突,不同的鍵可能會產生相同的bucket location,在使用put(key,value)時, 如果兩個鍵產生了相同的bucket location,由於LinkedList的長度是可變的, 所以會在該LinkedList中再增加一項Entry Object,其中儲存著key和value。

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

最後對LinkedList進行線性查詢的部分會比較慢,但是,如果雜湊函式好的話,陣列的每個位置就只有較少的值, 因此不是查詢整個LinkedList,而是快速地跳到陣列的某個位置,只對很少的元素進行比較,這就是HashMap會如此快的原因。

在知道了雜湊的原理後我們可以自己實現一個簡單的HashMap(例子來源於《Java程式設計思想(第四版)》)

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()方法的最佳實踐:

把某個非零的常數值,比如17,儲存在一個名為result的int型別中。

對於物件中的每個關鍵域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()

如果該域是一個數組,則要把每一個元素當作單獨的域來處理,也就是說遞迴地應用上述原則

按照公式: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;

    }

}

參考:

Javin Paul, How HashMap works in Java

http://javarevisited.blogspot.jp/2011/02/how-hashmap-works-in-java.html

機械工業出版社, 《Java程式設計思想(第四版)》

https://book.douban.com/subject/2061172/

機械工業出版社, 《Effective Java》

https://book.douban.com/subject/3360807/

文/HelloListen(github作者)
原文連結:https://github.com/HelloListen/Secret/blob/master/content/post/2016/05/java-hashmap-hashcode-hash.md

著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“csdn作者”。