区别
1. 继承的父类不同
- HashMap 继承了 AbstractMap,AbstractMap又实现了 Map 接口
- Hashtable 继承了 Dictionary
/*
* HashMap源码
*/
//HashMap继承了AbstractMap
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {}
//AbstractMap又实现了Map接口
public abstract class AbstractMap<K,V> implements Map<K,V> {}
/*
* Hashtable源码
*/
//Hashtable继承了Dictionary
Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable {}
2. 初始化数组长度,和扩容时的增量不同
- HashMap 默认的初始数组长度是 16,默认的加载因子是 0.75,每次扩容变成之前数组的 2 倍长度。
- Hashtable 默认的初始数组长度是 11,默认的加载因子是 0.75,每次扩容是之前数组的 2 倍长度加 1。
/*
* HashMap源码
*/
//HashMap 中的扩容方法resize(),旧数组长度*2得到新数组长度
final Node<K,V>[] resize() {
... ...
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
... ...
}
/*
* Hashtable源码
*/
//Hashtable中的扩容方法rehash(),旧数组长度左移1位加1得到新数组长度
protected void rehash() {
... ...
int newCapacity = (oldCapacity << 1) + 1;
... ...
}
3. 是否允许存储空值不同
- HashMap 允许value为空,只能有一个key为空。
- 而Hashtable 的 key,value 都不允许为空,在源码中的 put 方法里限制了如果 value 为空则抛出空指针异常,但是并没有限制 key,它的 hash 值就是 key.hashCode( ),对象为空调用 hashCode( ) 方法会抛出空指针异常。
/*
* HashMap源码
*/
//HashMap如果key为空,则会放在数组下标为0的Node桶里,根据put方法的流程新传入的key为null的键值
//会覆盖掉之前的,所以最多只会有一个key的为null
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/*
* Hashtable源码
*/
//Hashtable中hash值就是key的hashCode,对象为空的话hashCoe()方法会报空指针异常,对于value有了约束,为空也会抛出空指针异常
public synchronized V put(K key, V value) {
... ...
if (value == null) {
throw new NullPointerException();
}
... ...
int hash = key.hashCode();
... ...
}
4. 获取Hash值和数组下标的方法不同
- HashMap 的 hash( ) 方法会把key的 hashCode 与它的高16位异或,目的是使它的低16位更加散列,减少哈希冲突,提高性能。HashMap 获取数组下标通过 (数组长度 - 1) & hash 计算得到,实际上就是 hash %(数组长度)。
- Hashtable 的 hash 值由 key.hashCode( ) 得到,数组下标是先把 (hash & 0x7FFFFFFF => 取前31位二进制数,是为了避免出现整型溢出吗?),再对数组长度取模。
/*
* HashMap源码
*/
//HashMap的hash()方法会把key的hashCode与它的高16位异或
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//HashMap获取数组下标通过 (数组长度 - 1) & hash 计算得到
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
... ...
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
... ...
}
/*
* Hashtable源码
*/
//Hashtable的hash值由key.hashCode()得到,数组下标是先把 (hash & 0x7FFFFFFF) 再对数组长度取模
public synchronized V put(K key, V value) {
... ...
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
... ...
}
5. 底层数据结构不同
- HashMap 在 JDK1.8 中底层数据结构变成了数组加链表加红黑树,数组长度在大于 8 并 满足一定条件下升级成红黑树 ,比链表在查找性能上更加优秀。
- Hashtable 底层采用数组加链表的结构。
6. 扩容方法不同
- HashMap 在扩容时会生成两个链表或者红黑树,依次插入新数组的 当前坐标 和 当前坐标 + 旧数组长度 的位置。 - HashMap 扩容流程
- Hashtable 在扩容时每个结点依次做取余运算得到新数组下标。
/*
* Hashtable源码
*/
//Hashtable在扩容rehash()时每个结点的新数组下标为 (e.hash & 0x7FFFFFFF) % newCapacity
protected void rehash() {
... ...
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
... ...
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
... ...
}
}
}
7. 线程安全问题
- HashMap 不是线程安全的集合,源码中没有进行任何相关处理。
- Hashtable 是线程安全的,他在 put,remove 等可能存在线程安全的方法上加了 synchronized 关键字,锁住了当前对象。
/*
* Hashtable源码
*/
public synchronized V put(K key, V value) {}
public synchronized V remove(Object key) {}
8. 性能存在差距
- HashMap 是线程不安全的,先天性能优于 Hashtable,而且在确定数组下标,hash 方法,扩容 等方法中利用位运算来提升计算时的性能,当哈希冲突严重的时候红黑树的结构也会使查找性能更加优秀。
- Hashtable 采用了 synchronized 修饰 put,remove方法,锁住了当前 Hashtable 的实例化对象,多线程争抢锁激烈时性能会很差,数据结构也更加老旧,有种被舍弃的感觉。
- Hashtable 基本已经被弃用,需要考虑线程安全的时候会选择 ConcurrentHashMap。ConcurrentHashMap 数据结构和 HashMap 相同,在 JDK1.8 中改为数组加链表加红黑树的数据结构。与 Hashtable 把 synchronized 关键字加到方法上不同,后者采用 CAS(compare and swap)加上 synchronized 锁住某一个 Node结点。当插入删除在 Node 的第一个结点处就能完成操作时,一般直接采用 CAS 而避免使用锁产生不必要开销,只有在必要的时候才会加上 synchronized 关键字锁上当前 Node 结点。