作者:付佳豪



在面试的时候,java集合最容易被问到的知识就是HashMap与Hashtable的比较,通常我们也很容易回答出一下几点:

1、HashMap是线程不安全的,在多线程环境下会容易产生死循环,但是单线程环境下运行效率高;Hashtable线程安全的,很多方法都有synchronized修饰,但同时因为加锁导致单线程环境下效率较低。

2、HashMap允许有一个key为null,允许多个value为null;而Hashtable不允许key或者value为null。

如果你觉得会到处这两点就对HashMap、HashTable有很深的了解的话,那就大错特错!本文将基于JDK1.8的HashMap和Hashtable的源码进行详细的比较。

构造函数的比较

HashMap:



public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}

//该方法返回大于等于cap的最小2次幂的整数
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}


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);
}


可以看出HashMap的底层数组的长度必须为2^n,这样做的好处是为以后的hash算法做准备,而Hashtable底层数组的长度可以为任意值,这就造成了当底层数组长度为合数的时候,Hashtable的hash算法散射不均匀,容易产生hash冲突。所以,可以清楚的看到Hashtable的默认构造函数底层数组长度为11(质数),至于为什么Hashtable的底层数组用质数较好,请Hash算法的比较

hash算法的区别

HashMap:



static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//手写的,源码在不存在这一句,但是原理是类似的,详情可以去看putVal方法
int i = (table.length-1) & hash(key)


HashMap的hash算法通过非常规的设计,将底层table长度设计为2^n(合数),这是HashMap的一处优化。它使用了&运算来代替%运算以减少性能上面的损耗。为何&运算可以代替%运算呢?

如果两个整数做&运算,实质是两个整数转换为2进制数后每一个bit位的分别做&运算,所以其最终的运算结果的值不会超过最小的那个数,这个时候只需要搞清楚三点就能明白其实现原理:

1、合数2^n转换为2进制的数之后,最高位为1其余的位数都为0,比如16-->10000,32-->100000。那么,2^n-1转换为2进制后,所有的bit位都为1,比如31-->11111,127-->1111111。所以,hashcode与(2^n-1)做&运算每一个bit位都可以保持原来的值。

2、当hash()方法得到的值<=(table.length-1),其运算结果就在[0,table.length-1]范围内均匀散射。当hash()方法得到的值小于table.length-1的时候,运算结果就是该方法的原值。当hash()方法得到的值等于table.length-1的的时候,运算结果为0。

3、当hash()得到的值>(table.length-1),此时table.length-1为较小的数,所以&运算的结果还是在[0,table.length-1]之间。具体实现是这样的,table.length-1转化为2进制的数之后位数小于hash()方法得到的2进制数,所以它的高位只能用0去补齐,又由于&运算的特性,只要有一个为0那么都为0,所以此时相当于转化为情况1。

而hash()方法的具体作用是使得table的length较小的时候高低bit都能参与运算,具体分析请参考:​​https://tech.meituan.com/java-hashmap.html​

Hashtable:



int hash = key.hashCode();
//0x7FFFFFFF转换为10进制之后是Intger.MAX_VALUE,也就是2^31 - 1
int index = (hash & 0x7FFFFFFF) % tab.length;


很容易看出Hashtable的hash算法首先使得hash的值小于等于整型数的最大值,再通过%运算实现均匀散射。

由于计算机是底层的运算是基于2进制的,所以HashMap的hash算法使用&运算代替%运算,在运算速度上明显HashMap的hash算法更优

扩容的机制的区别

因为无论是HasHMap或者HashTable的扩容都是基于底层的hash算法的,所以将扩容机制放在hash算法部分讲。

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; // 将阈值扩大为2倍
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // 当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)