【JDK原始碼分析】淺談HashMap的原理

安全劍客發表於2020-03-07
在 HashMap 中存放的一系列鍵值對,其中鍵為某個我們自定義的型別。放入 HashMap 後,我們在外部把某一個 key 的屬性進行更改,然後我們再用這個 key 從 HashMap 裡取出元素,這時候 HashMap 會返回什麼?

【JDK原始碼分析】淺談HashMap的原理【JDK原始碼分析】淺談HashMap的原理

1. 特性

我們可以用任何類作為HashMap的key,但是對於這些類應該有什麼限制條件呢?且看下面的程式碼:

public class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
}
MaptestMap = new HashMap<>();
testMap.put(new Person("hello"), "world");
testMap.get(new Person("hello")); // ---> null

本是想取出具有相等欄位值Person類的value,結果卻是null。對HashMap稍有了解的人看出來——Person類並沒有override hashcode方法,導致其繼承的是Object的hashcode(返回是其記憶體地址),兩次new出來的Person物件並不equals——這也是為什麼在工程專案中常用不變類(如String、Integer等)做為HashMap的key的原因。那麼,HashMap是如何利用hashcode給key做索引的呢?

2. 原理

首先,我們來看《Thinking in Java》中一個簡單HashMap的實現方案:

//: containers/SimpleHashMap.java
// A demonstration hashed Map.
import java.util.*;
import net.mindview.util.*;
public class SimpleHashMapextends AbstractMap{
  // Choose a prime number for the hash table size, to achieve a uniform distribution:
  static final int SIZE = 997;
  // You can't have a physical array of generics, but you can upcast to one:
  @SuppressWarnings("unchecked")
  LinkedList<1mapentry>[] buckets =
    new LinkedList[SIZE];
  public V put(K key, V value) {
    V oldValue = null;
    int index = Math.abs(key.hashCode()) % SIZE;
    if(buckets[index] == null)
      buckets[index] = new LinkedList<1mapentry>();
    LinkedList<1mapentry> bucket = buckets[index];
    MapEntrypair = new MapEntry(key, value);
    boolean found = false;
    ListIterator<1mapentry> it = bucket.listIterator();
    while(it.hasNext()) {
      MapEntryiPair = it.next();
      if(iPair.getKey().equals(key)) {
        oldValue = iPair.getValue();
        it.set(pair); // Replace old with new
        found = true;
        break;
      }
    }
    if(!found)
      buckets[index].add(pair);
    return oldValue;
  }
  public V get(Object key) {
    int index = Math.abs(key.hashCode()) % SIZE;
    if(buckets[index] == null) return null;
    for(MapEntryiPair : buckets[index])
      if(iPair.getKey().equals(key))
        return iPair.getValue();
    return null;
  }
  public Set<1map.entry> entrySet() {
    Set<1map.entry> set= new HashSet<1map.entry>();
    for(LinkedList<1mapentry> bucket : buckets) {
      if(bucket == null) continue;
      for(MapEntrympair : bucket)
        set.add(mpair);
    }
    return set;
  }
  public static void main(String[] args) {
    SimpleHashMapm =
      new SimpleHashMap();
    m.putAll(Countries.capitals(25));
    System.out.println(m);
    System.out.println(m.get("ERITREA"));
    System.out.println(m.entrySet());
  }
}

SimpleHashMap構造一個hash表來儲存key,hash函式是取模運算Math.abs(key.hashCode()) % SIZE,採用連結串列法解決hash衝突;buckets的每一個槽位對應存放具有相同(hash後)index值的Map.Entry,如下圖所示:
【JDK原始碼分析】淺談HashMap的原理【JDK原始碼分析】淺談HashMap的原理
JDK的HashMap的實現原理與之相類似,其採用鏈地址的hash表table儲存Map.Entry:

/**
 * The table, resized as necessary. Length MUST Always be a power of two.
 */
transient Entry[] table = (Entry[]) EMPTY_TABLE;
static class Entryimplements Map.Entry{
    final K key;
    V value;
    Entrynext;
    int hash;
    …
}

Map.Entry的index是對key的hashcode進行hash後所得。當要get key對應的value時,則對key計算其index,然後在table中取出Map.Entry即可得到,具體參看程式碼:

public V get(Object key) {
    if (key == null)
        return getForNullKey();
    Entryentry = getEntry(key);
    return null == entry ? null : entry.getValue();
}
final EntrygetEntry(Object key) {
    if (size == 0) {
        return null;
    }
    int hash = (key == null) ? 0 : hash(key);
    for (Entrye = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

可見,hashcode直接影響HashMap的hash函式的效率——好的hashcode會極大減少hash衝突,提高查詢效能。同時,這也解釋開篇提出的兩個問題:如果自定義的類做HashMap的key,則hashcode的計算應涵蓋建構函式的所有欄位,否則有可能得到null。

原文地址:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31559985/viewspace-2679002/,如需轉載,請註明出處,否則將追究法律責任。

相關文章