目录
1.前言
HashMap与HashTable的区别是面试过程中最容易被问到的问题,如果直接背,很容易就忘记,所以自己通过对源码的了解来总结出HashMap与HashTable的不同,如果与别的文章有雷同,纯属巧合。
JDK版本:1.8
2.HashMap与HashTable的区别
2.1继承的父类不同
public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
}
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
}
看上面的源码,HashTable继承的是Dictionary类,而HashMap继承的是AbstractMap类。不过两者都实现了Map、Clonable和Serializable接口。
2.2线程安全性不同
HashTable是线程安全的,内部的方法基本都经过synchronized关键字进行修饰,而HashMap是线程不安全的。正是因为加了锁的原因,HashTable的效率低于HashMap。另外,HashTable 基本被淘汰,不要在代码中使用它;
2.3初始化和扩容机制不同
HashTable的初始化:
public Hashtable(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal Load: "+loadFactor);
if (initialCapacity==0)
initialCapacity = 1;
this.loadFactor = loadFactor;
table = new Entry<?,?>[initialCapacity];
threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}
public Hashtable() {
this(11, 0.75f);
}
看上面HashTable的源码,当我们初始化一个HashTable对象时,如果不指定初始化大小和加载因子,那么默认初始化大小为11,加载因子为0.75,如上面第11、12和16行代码。
HashMap的初始化:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
如上面的源码,当创建一个HashMap实例对象时,如果不指定初始容量和加载因子时,默认的加载因子为0.75,这时并没有创建table。当用户向HashMap中插入数据时,才会创建table。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
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;
}
当用户向HashMap中插入数据时会调用put方法,而put方法会调用putVal方法实现插入数据。看putVal方法中的第3行代码和第4行代码,一开始时table是为null或者table的长度为0时,就会调用resize方法。putVal方法中倒数第四行代码,此时是HashMap需要扩容时调用的代码。即resize是HashMap初始化和扩容的具体实现方法。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
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)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
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);
}
一开始的时候table是空的,即resize方法中oldTab为空,那么oldCap为0,即一开始调用resize方法时会执行倒数第九行代码处,而DEFAULT_INITIAL_CAPACITY的值刚好就是16,所以HashMap初始化的大小就是16个。
HashTable的扩容机制:
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;
}
HashTable的扩容机制是通过rehash方法实现的,每次扩容时先获取HashTable的长度,然后向左移一位并加1,即扩容到原来的两倍加1;
HashMap的扩容机制:
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)
newThr = oldThr << 1; // double threshold
}
HashMap的扩容依然调用resize函数实现的,每次需要扩容时,先获取HashMap原来的长度,然后再向左移动一位,即扩容为原来的两倍。如上面倒数第三行代码。
2.4对null值支持不同
在HashTable中,key和value都不能为null值;在HashMap中,key可以为null,但只能允许存在一个,而value不仅可以为null,还可以存在多个。
2.5对外的接口不同
HashMap中没有contains方法,但有containsValue方法和containsKey方法;HashTable则保留了contains方法,效果同containsValue,还包括containsValue和containsKey方法。除此之外,HashTable还多提供了elments() 方法,该方法继承自HashTable的父类Dictionnary。elements() 方法用于返回此HashTable中的value的枚举。
2.6底层数据结构不同
在jdk1.7版本中,HashTable与HashMap底层都是数组+链表。到了JDK8版本中,HashMap引入了红黑树的结构。如果entry数组中某个位置链表的冲突数据数量小于8时,则以链表的形式存在;如果冲突数据的数量大于8时,则将链表转换成红黑树存储,当冲突数量小于6时,又转为链表存储。
2.7支持的遍历种类不同
HashMap只支持Iterator遍历,而HashTable支持Iterator和Enumeration 两种方式遍历。
HashMap的迭代器(Iterator)是fail-fast迭代器,而HashTable的Enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常,而HashTable 则不会。
2.8计算hash值的方式不同
HashTable的哈希值计算:
@SuppressWarnings("unchecked")
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;
}
HashTable的hash值直接使用的key的hash值,不过需要去掉符号位(即& 0x7FFFFFFF),获取数据的下标时只需要对HashTable的长度进行取模。
HashMap的哈希值计算:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
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;
HashMap的哈希值计算则是先判断key是否为空,如果为空,哈希值直接为0,否则先获取key的哈希值,然后在异或上右移16位的key的哈希值(即h >>> 16)。而HashMap的下标计算公式为:(table.length-1)&hash。
3.总结
两者的不同点:
- 继承的父类不同。HashTable继承的是Dictionary类,而HashMap继承的是AbstractMap类。
- 线程安全性不同。HashTable是线程安全的,而HashMap是线程不安全的。HashTable在方法上面加上了synchronized关键字。也因如此,HashTable的效率不如HashMap。
- 初始容量和扩容机制不同。HashTable默认初始化大小为11,加载因子为0.75,每次扩容时扩大为原来的2倍+1;HashMap默认初始化大小为16,加载因子为0.75,每次扩容时扩大为原来的2倍。
- 对null值支持不同。HashTable的key和value都不能为null;HashMap的key可以为null,但只能存在一个,而可以有多个value为null。
- 对外的接口不同。HashTable比HashMap多了elements和contains方法,contains方法与containValue方法一致。
- 底层数据结构不同。在JDK1.7时,两者底层都是采用数组+链表的形式。到了JDK1.8,HashMap引入了红黑树的结构。当链表长度大于8时,链表转换为红黑树存储,数据量小于6时才会转回链表。
- 支持的遍历种类不同。HashMap只支持Iterator遍历,而HashTable支持Iterator和Enumeration 两种方式遍历。
- 计算hash值的方式不同。HashTable的hash值使用的是key的hash值(去掉符号位),而HashMap则是通过计算生成(key.hash^(key.hash>>>16))。