HashTable与HashMap的区别于联系是在面试中经常遇到的问题,最近被这个问题困扰,看了几篇blog,总结一下。
1.出现的时间
从出现的时间来说,HashTable产生于JDK 1.1,而HashMap产生于JDK 1.2。从时间的维度上来看,HashMap要比HashTable出现得晚一些。其实现在HashTable已经废弃了,如果现在写代码,都不用HashTable了,至于为什么要一直更新,因为前面有很多代码用到,现在必须对其升级维护。
2.继承的类与接口以及对外接口(API)
从上面的图可以看到,虽然都实现了Map、Cloneable、Serializable三个接口。但是HashMap继承自抽象类AbstractMap,而HashTable继承自抽象类Dictionary。其中Dictionary类是一个已经被废弃的类。不过他们的底层实现基本是一致的,都是基于数组(Entry类型)加链表这种数据结构实现的,称为“拉链法”实现哈希表,如下图所示。
同时我们看到HashTable比HashMap多了两个公开方法。一个是elements,这来自于抽象类Dictionary,鉴于该类已经废弃,所以这个方法也就没什么用处了。另一个多出来的方法是contains,这个多出来的方法也没什么用,因为它跟containsValue方法功能是一样的。
以下代码及注释来自java.util.HashTable
public synchronized boolean contains(Object value) {
if (value == null) {
throw new NullPointerException();
}
Entry tab[] = table;
for (int i = tab.length ; i-- > 0 ;) {
for (Entry<K,V> e = tab[i] ; e != null ; e = e.next) {
if (e.value.equals(value)) {
return true;
}
}
}
return false;
}
public boolean containsValue(Object value) {
return contains(value);
}
所以从公开的方法上来看,这两个类提供的,是一样的功能。都提供键值映射的服务,可以增、删、查、改键值对,可以对建、值、键值对提供遍历视图。支持浅拷贝,支持序列化。
3.两者的构造方法
HashTable和HashMap的构造方法:两者的构造方法都是一样的,以下4种(以HashMap为例)。
HashMap()
构造一个空的 HashMap ,默认初始容量(16)和默认负载系数(0.75)。
HashMap(int initialCapacity)
构造一个空的 HashMap具有指定的初始容量和默认负载因子(0.75)。
HashMap(int initialCapacity, float loadFactor)
构造一个空的 HashMap具有指定的初始容量和负载因子。
HashMap(Map<? extends K,? extends V> m)
构造一个新的 HashMap与指定的相同的映射 Map 。
HashMap的一个实例有两个影响其性能的参数: 初始容量(initialCapacity)和负载因子(loadFactor) 。 容量是哈希表中的桶(bucket)数,初始容量只是创建哈希表时的容量。 负载因子是在容量自动增加之前允许哈希表得到满足的度量。 当在散列表中的条目的数量超过了负载因数和初始容量的乘积,哈希表被重新散列 (即内部数据结构被重建),使得哈希表具有桶的大约两倍。
作为一般规则,默认负载因子(0.75)提供了时间和空间成本之间的良好折中。 更高的值会降低空间开销,但会增加查找成本(反映在HashMap类的大部分操作中,包括get和put )。 在设置其初始容量时,应考虑地图中预期的条目数及其负载因子,以便最小化重新散列操作的次数。 如果初始容量大于最大条目数除以负载因子,则不会发生重新排列操作。
4、HashTable与HashMap主要方法的实现
HashTable和HashMap的API中提供了很多的方法,其中主要两个方法是put()和get():
put()方法:
思路:
<1>根据key计算可以key的hashCode,然后计算出hashCode在数组中对应的bucket的位置,如果该位置的key为null,那么在这个位置创建一个新的Entry类型的Node:
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
<2>如果这个位置的key不为null,那么就要判断待添加的元素和该位置的元素是否相同,如果相同的话,就把key对应的旧节点e替换为新的节点:
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
<3>如果该节点是一个树节点的实例,那说明HashMap的底层已经转为数组加红黑树的结构,那么把该节点添加到红黑树中:
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
<4>如果以上都不是,那么开始对该位置指向的链表遍历,对每一个节点,如果他们的hashCode相同且key相同,那么退出遍历,该节点就是要被替换成新节点的节点e,对于这个节点,做如下处理:将该节点的value换成新的value,并返回旧的value。
if (e != null)
{ // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
<5>如果遍历完该位置的链表都没有找到重复的节点(hashCode一致且key相同),那么在链表尾部添加新节点,并且如果添加新节点后,链表长度大于TREEIFY_THRESHOLD那么将链表转换为红黑树的结构:
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
在JDK1.8.0_102中,TREEIFY_THRESHOLD等于8,也就是说,当链表长度大于8时,将链表转换为红黑树的结构,这样当该位置(bucket)的节点数很多时,访问效率会提高很多。
<6>最后,modcount要加1,因为modcount是用来记录HashMap的修改操作的次数的,用于检测该HashMap是否被多线程修改,如果多线程的modcount不一致,说明该HashMap被多线程修改,那么立刻抛出异常。然后,如果size(节点数/键值对数)大于threshold,那么就要进行扩容resize()。
HashMap源码:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
HashTable源码:因为HashTable的key和value都不允许为null,并且HashTable不会转换为红黑树的结构,因此要简单很多。
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;
int hash = key.hashCode();
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;
}
在put()方法中,有两个地方需要注意:
<1>在put()一个键值对时,先计算hashCode,如果hashCode相同再判断key和已有的key是否相同,在判断是否相同时,有两种情况,一个是添加的key和原有的key是同一个对象,或者不是同一个对象但是equals()返回true,这两种情况都是判断相同的。此外,从源代码中可以看出HashMap的key可以为null:
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
但是HashTable的key是不能为null的,因为HashTable中直接调用key的hashCode()
方法,如果key为null会抛出异常:
if (value == null) {
throw new NullPointerException();
}
<2>扩容:HashTable使用rehash(),HashMap使用resize(),当键值对的数量超过threshold=initialCapacity*loadFactor 的时候,就会自动调用rehash()或者resize()对table进行扩容,并重新组织table,以便减少hash冲突,使HashTable或HashMap更有效率。
HashMap源码:
因为要判断oldTab是否为null或者长度为0,所以HashMap的新容量的计算相对麻烦一点。HashMap的新容量=旧容量*2,新threshold = 旧threshold*2。然后在oldTab中从头向后逐个取出节点,根据节点key的hashCode计算节点在newTab中的位置,并把oldTab中的节点指向null。
/**
* 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; //判断oldTab是否为null,然后返回oldTab的长度
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)
newThr = oldThr << 1; // double threshold ,HashMap的新容量=旧容量*2,新threshold = 旧threshold*2
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr; //如果旧容量==0, 且就threshold大于0,那么新容量等于旧threshold
else { // zero initial threshold signifies using defaults
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"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; //根据新容量创建newTab
table = newTab;
//从oldTab中从前向后逐个取出节点,计算节点key的hashCode在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
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
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;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
HashTable源码:
HashTable扩容后 新容量是原容量的2倍+1,新threshold为新容量*loadFactor。然后在oldMap中从后向前取出节点,重新计算节点在newMap中的hashCode,放进newMap中,并把oldMap中的节点指向null。
/**
* 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
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;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++; //扩容也是修改操作,modcount加1
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
//把oldMap的节点重新计算hashCode放进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;
}
}
}
get()方法:
HashMap源码:根据给出的key计算hashCode,然后计算在该HashMap中对应的bucket位置,如果找到fail位置,并且key.equals()返回true,那么返回该可以映射的value,如果没有找到符合条件的key,那么返回null,但是这里要说明,返回null也不一定是没有找到符合条件的null,也可能是这个key映射的value本来就是null,那么这个时候就需要用containsKey()这个方法来区别这两种情况。
/**
* Returns the value to which the specified key is mapped,
* or {@code null} if this map contains no mapping for the key.
*
* <p>More formally, if this map contains a mapping from a key
* {@code k} to a value {@code v} such that {@code (key==null ? k==null :
* key.equals(k))}, then this method returns {@code v}; otherwise
* it returns {@code null}. (There can be at most one such mapping.)
*
* <p>A return value of {@code null} does not <i>necessarily</i>
* indicate that the map contains no mapping for the key; it's also
* possible that the map explicitly maps the key to {@code null}.
* The {@link #containsKey containsKey} operation may be used to
* distinguish these two cases.
*
* @see #put(Object, Object)
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//如果first不是符合条件的key,那么对后面的bucket,如果是树,则查询树的节点key是否
//符合条件,如果是链表,则遍历链表中的节点,查看是否有符合条件的key
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null; //如果没有查找到则返回null
}
HashTable源码:因为HashTable的key和value都不允许为null,并且HashTable不会转换为红黑树的结构,因此要简单很多,只要遍历每个bucket的链表,查询是否有符合条件的key即可。
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
5、总结
<1>继承的类不同:HashTable继承Dictionary类,HashMap继承AbstractMap类;
<2>线程安全级别不同:HashTable是线程安全的类,每个方法都有Synchronized修饰,HashMap不是线程安全的,如果想要用HashMap创建线程安全的对象,那么可以用Collections接口的SynchrinizedMap()方法封装一个HashMap的对象,使之成为线程安全的对象。由于HashMap非线程安全,在只有一个线程访问的情况下,效率要高于HashTable。
Map m = Collections.synchronizedMap(new HashMap(...));
<3>是否允许null的要求不同
HashTable:key不允许为null,value不允许为null
HashMap:key允许为null,value允许为null
<4>在JDK1.8以后,HashMap在节点数大于TREEIFY_THRESHOLD(等于8)时,底层实现从数组加链表转换为数组加红黑树;但是HashTable没有这种操作。
<5>HashMap把HashTable的contains方法去掉了,改成containsvalue和containsKey,因为contains方法容易让人引起误解。而Hashtable有contains方法、containsvalue方法和containsKey方法,其中contains方法和containsvalue方法是一样的。
<6>两者通过hash值散列到hash表的算法不一样:
HashTbale是古老的除留余数法,直接使用hashcode
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
而HashMap是强制容量为2的幂,重新根据hashcode计算hash值,在使用hash 位与 (hash表长度 – 1),也等价取模,但更加高效,取得的位置更加分散,偶数、奇数保证了都会分散到。前者就不能保证。
int hash = hash(k);
int i = indexFor(hash, table.length);
static int hash(Object x) {
int h = x.hashCode();
h += ~(h << 9);
h ^= (h >>> 14);
h += (h << 4);
h ^= (h >>> 10);
return h;
}
static int indexFor(int h,int length) {
return h & (length-1);
}
<7>HashMap的迭代器(Iterator)是fail-fast迭代器,而HashTable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。
*关于fail-fast机制:fail-fast是通过modcount参数实现的,在HashMap中每当有会修改HashMap结构的操作被执行,那么modcount加1,modcount是volatile的,因此是线程可见的,在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map。而HashTable并没有这个参数,因此没有fail-fast机制。
参考博客