Java——HashMap和HashTable的区别
Java HashMap和HashTable的区别
1. 继承的父类
都实现了Map
、Cloneable
(可复制)、Serializable
(可序列化)接口。
HashMap
: 继承自AbstractMap
类。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
}
HashTable
: 继承自Dictionary
类。
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
}
2. 线程安全性
HashMap
: 线程不安全,效率高。在多线程并发的环境下,可能会产生死循环,数据覆盖等问题。
参考:
https://www.jianshu.com/p/e2f75c8cce01
https://www.cnblogs.com/developer_chan/p/10450908.html
HashTable
: 线程安全,效率低。
3. null值问题
HashMap
: 允许null
值作为key
或value
。只有一个key
可以为null
,可以多个null
为value
.
Map<Integer, Integer> map = new HashMap<>();
map.put(null, 12);
System.out.println(map.get(null)); // 12
map.put(1, null);
map.put(2, null);
System.out.println(map.get(1)); // null
System.out.println(map.get(2)); // null
HashTable
: 不允许null
值作为key
或value
Hashtable<Integer, Integer> hashtable = new Hashtable<>();
hashtable.put(null, 12); // java.lang.NullPointerException
hashtable.put(1, null); // java.lang.NullPointerException
4. 初始容量及扩容方式
HashMap
: hash
数组默认大小为16,扩容方式:16 * 2
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 将阈值扩大为2倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 当threshold的为0的使用默认的容量,也就是16
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 新建一个数组长度为原来2倍的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//HashMap在JDK1.8的时候改善了扩容机制,原数组索引i上的链表不需要再反转。
// 扩容之后的索引位置只能是i或者i+oldCap(原数组的长度)
// 所以我们只需要看hashcode新增的bit为0或者1。
// 假如是0扩容之后就在新数组索引i位置,新增为1,就在索引i+oldCap位置
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 新增bit为0,扩容之后在新数组的索引不变
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else { //新增bit为1,扩容之后在新数组索引变为i+oldCap(原数组的长度)
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
//数组索引位置不变,插入原索引位置
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
//数组索引位置变化为j + oldCap
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
HashTable
: hash
数组默认大小为11,扩容方式:old * 2 + 1
/**
* Increases the capacity of and internally reorganizes this
* hashtable, in order to accommodate and access its entries more
* efficiently. This method is called automatically when the
* number of keys in the hashtable exceeds this hashtable's capacity
* and load factor.
*/
@SuppressWarnings("unchecked")
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
// 扩容为 old * 2 + 1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
// 新建长度为old * 2 + 1的数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
// 使用头插法将链表反序
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
5. 遍历方式
Hashtable
、HashMap
都使用了Iterator
。而由于历史原因,Hashtable
还使用了Enumeration
的方式。
HashMap
实现 Iterator
,支持fast-fail
,当有其它线程改变了HashMap
的结构(增加,删除,修改元素),将会抛出ConcurrentModificationException
。不过,通过Iterator
的remove()
方法移除元素则不会抛出ConcurrentModificationException
异常。Hashtable
的Iterator
遍历支持fast-fail
,用 Enumeration
不支持fast-fail
。
6. 计算hash
值方式
HashMap
: 根据元素的key
计算出一个hash
值,然后再用这个hash
值来计算得到最终的位置。
HashMap
为了提高计算效率,将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。效率虽然提高了,但是hash
冲突却也增加了。因为它得出的hash
值的低位相同的概率比较高,而计算位运算为了解决这个问题,HashMap
重新根据hashcode
计算hash
值后,又对hash
值做了一些运算来打散数据。使得取得的位置更加分散,从而减少了hash
冲突。当然了,为了高效,HashMap
只做了一些简单的位处理。从而不至于把使用2的幂次方带来的效率提升给抵消掉。例如通过h ^ (h >>> 16)
无符号位右移。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 类似的原理 (table.length-1) & hash(key)
public native int hashCode();
HashTable
: Hashtable
直接使用key
对象的hashCode
。hashCode
是JDK
根据对象的地址或者字符串或者数字算出来的int
类型的数值。然后再使用除留余数法
来获得最终的位置。然而除法运算是非常耗费时间的,效率很低。
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
// 直接使用 key对象的 hashcode
int hash = key.hashCode();
// 0x7FFFFFFF转换为10进制之后是Intger.MAX_VALUE,也就是2^31 - 1
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}