HashMap與HashTable的區別

HashMap與HashTable有什麼區別?在面試中經常會問到這樣的問題。於是,通過查閱一些資料,總結一下寫下這篇博客。

區別1:兩者誕生的時間 不同。

 HashTable產生於JDK1.1,而HashMap產生於JDK1.2。從時間上來說,HashMap要比HashTable出現得晚一些。

區別2:類的繼承體系不同

可以看一下下面兩幅圖(HashMap和HashTable的類繼承圖)



 從上面兩幅圖中可以看出。HashMap和HashTable都實現了Cloneable、Serializable和Map接口。但是不同之處是

HashMap繼承了抽象類AbstractMap,而HashTable繼承了抽象類Dictionary(此類已經是一個被廢棄的類)

區別3:對於Null key和 Null Value的處理不同

 HashMap支持null鍵和null值的,而HashTable在遇到null鍵和null值時會拋出空指針異常。這主要是因爲代碼中對null值

的處理。可以看一下代碼:

以下是HashTable的代碼:

public synchronized V put(K key, V value) {

  // 如果value爲null,拋出NullPointerException
  if (value == null) {
      throw new NullPointerException();
  }

  // 如果key爲null,在調用key.hashCode()時拋出NullPointerException

  // ...
}
以下是HashMap的代碼:

public V put(K key, V value) {
  if (table == EMPTY_TABLE) {
      inflateTable(threshold);
  }
  // 當key爲null時,調用putForNullKey特殊處理
  if (key == null)
      return putForNullKey(value);
  // ...
}

private V putForNullKey(V value) {
  // key爲null時,放到table[0]也就是第0個bucket中
  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;
}

區別4:實現原理方面的不同:主要是在數據結構和算法層面

相同點(數據結構):HashMap和HashTable都使用哈希表來存儲鍵值對,在數據結構上基本上是相同,都創建了一個繼承自

Map.Entry的私有的內部類Entry,每一個Entry對象都表示存儲在Hash表中的鍵值對。

Entry對象唯一表示一個鍵值對,有四個屬性:

-K key 鍵對象

-V value 值對象

-int hash 鍵對象的hash

-Entryentry 指向鏈表中下一個Entry對象,可爲null,表示當前Entry對象在鏈表尾部

 可以通過下圖更加清晰的看出存儲的數據結構


這樣就可以得出這樣的結論:HashMap和HashTable內部用Entry數組實現哈希表,而對於映射到同一個哈希桶

bucket(數組的同意位置)的鍵值對,使用Entry鏈表來存儲(解決hash衝突)。

算法(不同):

 上一節中已經說了HashMap和HashTable的內部數據結構。HashMap和HashTable還需要有算法來將一個給定的key值計算出HashCode值,映射到確定的Hash桶(數組位置)需要有算法在hash桶的鍵值多到一定程度時,擴充hash表的大小(數組的大小)。本節主要是比較兩個類在算法層面是有什麼樣

的不同。

 初始容量大小和每次擴充容量大小的不同。先看一下代碼:

以下代碼及註釋來自java.util.HashTable

// 哈希表默認初始大小爲11
public Hashtable() {
  this(11, 0.75f);
}

protected void rehash() {
  int oldCapacity = table.length;
  Entry<K,V>[] oldMap = table;

  // 每次擴容爲原來的2n+1
  int newCapacity = (oldCapacity << 1) + 1;
  // ...
}


以下代碼及註釋來自java.util.HashMap

// 哈希表默認初始大小爲2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

void addEntry(int hash, K key, V value, int bucketIndex) {
  // 每次擴充爲原來的2n 
  if ((size >= threshold) && (null != table[bucketIndex])) {
     resize(2 * table.length);
}

從上面的代碼中可以看出來,對於HashTable,hash表的默認初始值是11,然後每次擴充爲原來的2n+1。

而HashMap的初始值是16,之後每次擴充爲原來的2倍。

也就是說HashTable的hash表容量儘量是素數、質數。而HashMap則總是使用2的冪作爲hash表的大小。

那它們這樣做有什麼優缺點那?

當hash表的大小爲質數時,簡單的取模哈希表的結果會更加的均勻,所以,從這一點上看,HashTable的哈希表大小的選擇會

更加的合理。但是另一方面,如果取模數是2的冪的話,我們可以使用另外一種方法會更加的方便:位運算,效率會大大高於

除法運算得到的結果。所以,從Hash計算的效率上,又是HashMap更勝一籌。

所以,hashmap爲了解決取模不均勻的問題,又對hash算法做了一些改動。具體我們看看,在獲取了key對象的hashCode之後,

HashTable和HashMap分別是怎樣將他們哈希到確定的hash桶(Entry數組位置)中的。

以下代碼及註釋來自java.util.HashTable

// hash 不能超過Integer.MAX_VALUE 所以要取其最小的31個bit
int hash = hash(key);
int index = (hash & 0x7FFFFFFF) % tab.length;

// 直接計算key.hashCode()
private int hash(Object k) {
  // hashSeed will be zero if alternative hashing is disabled.
  return hashSeed ^ k.hashCode();
}


以下代碼及註釋來自java.util.HashMap
int hash = hash(key);
int i = indexFor(hash, table.length);

// 在計算了key.hashCode()之後,做了一些位運算來減少哈希衝突
final int hash(Object k) {
  int h = hashSeed;
  if (0 != h && k instanceof String) {
      return sun.misc.Hashing.stringHash32((String) k);
  }

  h ^= k.hashCode();

  // This function ensures that hashCodes that differ only by【再哈希算法】
  // constant multiples at each bit position have a bounded
  // number of collisions (approximately 8 at default load factor).
  h ^= (h >>> 20) ^ (h >>> 12);
  return h ^ (h >>> 7) ^ (h >>> 4);
}

// 取模不再需要做除法
static int indexFor(int h, int length) {
  // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
  return h & (length-1);
}
我們可以通過上面的代碼可以看出,HashMap由於使用了2的冪次方,所以在位運算取模時,引入了hash衝突加劇的問題。爲了解決這個問題,HashMap調用了對象的hashCode()方法之後,又重新做了一些位運算再次打散數據(再hash)。


區別5:線程安全

HashTable是線程同步的,而HashMap不是的。可以看一下代碼:

以下代碼及註釋來自java.util.HashTable

public synchronized V get(Object key) {
  Entry tab[] = table;
  int hash = hash(key);
  int index = (hash & 0x7FFFFFFF) % tab.length;
  for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
      if ((e.hash == hash) && e.key.equals(key)) {
          return e.value;
      }
  }
  return null;
}

public Set<K> keySet() {
  if (keySet == null)
      keySet = Collections.synchronizedSet(new KeySet(), this);
  return keySet;
}
從代碼中我們可以看出來:在公開的方法中,HashTable中的方法都是用了synchronized描述符。







發佈了71 篇原創文章 · 獲贊 76 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章