Hashtable、HashMap、TreeMap 三者区别
比较点 | Hashtable | HashMap | TreeMap |
---|---|---|---|
key | 不能为null | 可以为 null | |
value | 不能为 null | 可以为 null | |
key | 一定情况下可以为 null | ||
有序性 | 无序 | 无序 | 有序 |
容量 | 初始 11 | 初始 16,且一定为 2 n 2^n 2n | |
扩容 | 2n+1 | 2n | |
线程安全性 | 线程安全 | 线程不安全 | |
hash 值计算 | 底层是模运算,效率较低 | 底层是移位运算 |
HashMap
HashMap
可以存储 null
的 key 和 value,作为 null
的 key 只能有一个,null
值可以有多个。
key 的唯一性也要求
null
只能有一个,这种理解很正常;如果把null
值当作与其他普通值一样看待,null
允许多个也不足为奇。
null
的 hash 值为 0,这是源码规定的:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
其他 value 的 hash 值与 hashCode 有关
JDK 1.8 的 HashMap 由数组 + 链表组成。
数组是 HashMap 的主体:
transient Node<K,V>[] table;
链表是为了解决哈希冲突而存在(拉链法):
p.next = newNode(hash, key, value, null);
使用 hash 值,通过 (n - 1) & hash
得到元素在数组中的索引:
tab[i = (n - 1) & hash]
hash 值也可以直接使用 key.hashCode()
,但是并没有,而是进行了 hash 方法进行扰动,防止一些糟糕的 hashCode()
,而且也满足了 null 值的插入需求:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
一个数与 0 做 “异或” 运算等于这个数与 0 做 “或” 运算,所以高 4 字节保留,低 4 字节与高 4 字节做异或运算。
当链表长度大于 8 时,会调用 treeifyBin()
方法。但这个方法也并非一定会将链表树化,只有当数组长度大于等于 64 的情况下,才会执行红黑树转换。
loadFactory 加载因子:控制数组存放数据的疏密程度,loadFactory 越趋近于 1,那么数组存放的数据越多,越密;loadFactory 越小(越趋近于0),存放的数据越系数。
默认值 0.75f 是官方给出的比较好的临界值
threshold:capacity * loadFactory,当 size >= threshold 时,需要考虑对数组扩容。
putVal
如果定位到的数组位置没有元素,就直接插入。
如果定位到的数组位置有元素,就要和插入的 key 比较。
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;
}
}
这段判断逻辑为为什么必须要增加 p.hash == hash
的逻辑? 个人觉得是 hash 值是由 hashCode() 方法计算得到,同时以 hashCode 和 equals 方法约束两个对象是否相同。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
resize
如果你的底层数组大小已经 >= MAXIMUM_CAPACITY,那么 threshold 将会设置为 Integer.MAX_VALUE
,这时候的 threshold 是比 capacity 大的。这体现在:1、当你初始化设置为一个 >= MAXIMUM_CAPACITY 的值,2. 或者运行时扩容达到 >= MAXIMUM_CAPACITY
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;
}
// 尝试扩容,如果扩容之后的大小没有达到最大值,且旧数组 oldCap >= DEFAULT_INITIAL_CAPACITY
// 那么,获得新的 new threshold
// 这里 newCap 和 newThr 都是双倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 比如,从 12 变为 24
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
// 如果构造 HashMap 的时候传入了 initCapacity,那么这里 oldThr 就是 initCapacity
newCap = oldThr;
else { // zero initial threshold signifies using defaults
// 第一次 put 的时候会进入该逻辑
newCap = DEFAULT_INITIAL_CAPACITY; // 默认大小 16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 阈值 12
}
if (newThr == 0) {
// 1. 如果是自定义 initCapacity,第一次会计算一下 newThr
// 2. 如果是自定义 initCapacity,大小偏小,大小没有达到 DEFAULT_INITIAL_CAPACITY,例如:1,2,4,8 此类,也会计算 newThr
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];
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
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;
}
TreeMap
TreeMap 关于 key 为 null 的说明:TreeMap 初始化时可以传入 Comparator 参数,但也可以不传入。如果没有传入,在 put 元素时,默认使用 key 实现的 Comparable 进行比较然后按顺序存储(如果 key 没有实现 Comparable,在 ClassCast 的时候直接抛出异常),当然,key 如果为 null,则调用方法 compare 直接空指针异常。
但是,如果传入 Comparator,那么在 Comparator 的方法 compare 中,小心地判断了 key 为 null 的情况,也是可以将 key 存储为 null 的。
ConcurrentHashMap
initTable
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
// sizeCtl 控制全局的 size 变量
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// 多线程尝试将 sizectl 从 sc 置为 -1,表示一种上锁
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
// 锁标记还原
sizeCtl = sc;
}
break;
}
}
return tab;
}
sizeCtl :默认为0,用来控制table的初始化和扩容操作
-1 代表table正在初始化
-N 表示 -N 对应的二进制的低 16 位数值为 M,此时有 M - 1 个线程进行扩容。
为什么 HashMap 允许 value 为 null? 而 ConcurrentHashMap 不允许 value 为 null?
无论哪种 Map,假设允许 value 存储 null 值,那么在调用 get 方法的时候,有两种可能性:
- 该 key 没有在 Map 中映射过
- 该 key 在 Map 中存在对应的 value,但是值为 null
但是,HashMap 在单线程的正确使用场景下,我们可以使用 containsKey(key)
方法来区分是否包含该 key
如果在多线程 ConcurrentHashMap
下,使用 get 方法得到 null,但我们不知道该 null 是不存在映射,还是 value 为 null,因此我们会调用 containsKey 方法判断,假设 get 与 containsKey 方法之间有其他线程进行了 put,我们就无法判断了。因此,ConcurrentHashMap 干脆直接禁止 value 为 null。
不过,建议,HashMap 也不要存储 null 值,就像 HashSet 底层的 HashMap 也会将 value 存储为一个标记对象,而不是一个 null。
至于 ConcurrentHashMap
为什么不支持 key 为 null,可能只能归咎于源码本身并不支持。