Java1.7 HashMap
Java1.7中的HashMap是经典的哈希表实现,即数组+链表。以下针对一些面试中常见的问题,通过解读源码寻找答案。
- 初始桶空间(16) 是在创建HashMap对象时开辟吗?
创造一个HashMap对象时,还未开辟16个默认的桶空间;第一次调用put方法时才会开辟空间。
参考源码,构造函数调用的init()
方法是空的。
// 构造函数
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); //16, 0.75
}
// 构造函数
public HashMap(int initialCapacity, float loadFactor) {
//if...
this.loadFactor = loadFactor;
threshold = initialCapacity;
init();
}
void init() {
}
- 为什么数组大小必须是2的幂(初始容量为2^4=16)?
2^n - 1 能得到二进制数全部为1的值,和哈希值进行按位与(&)运算就能快速地得到分布均匀的数组下标。例如:
n=4: 10000
-1
-------
1111
& 0101..1001 (32-bit)
-------------
1001
- 为什么负载因子默认值是0.75?
是在时间和空间上的折中:如果负载因子过大,则减少了空间的浪费,但增加了查找时间上的消耗。
参考源码,put()
方法中:
- 如果表为空,则调用
inflateTable()
方法开辟空间,其中调用roundUpToPowerOf2()
方法将容量上调至2的幂 - 调用
hash()
方法计算哈希值 - 调用
indexFor()
方法计算索引值(上述的按位与(&)运算) - 遍历数组中的链表
e
:如果找到key则覆盖并返回旧元素值;否则添加一个链表节点,其中如果数组大小>=容量*负载因子(0.75)
static final Entry<?,?>[] EMPTY_TABLE = {}; //空的表实例,当表未初始化时被所有对象所共享
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold); // 开辟空间 1*
}
//if (key == null)...
int hash = hash(key); // 计算哈希值 2*
int i = indexFor(hash, table.length); // 计算索引值(数组下标) 3*
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 如果找到key则覆盖
if (e.hash == hash && ((k=e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 否则,添加一个链表节点 4*
addEntry(hash, key, value, i);
}
// 1*
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize); //向上取整为2的幂
//...
table = new Entry[capacity];
//...
}
// 2* 计算哈希值,防止低位相同值很多的哈希碰撞
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) { //如果是String,用另一种方法(不是String提供的hashCode)来计算哈希值,避免潜在攻击
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
// 3*
static int indexFor(int h, int length) {
return h & (length-1);
}
// 4*
void addEntry(int hash, K key, V value, int bucketIndex) {
// 如果size>=capacity*loadFactor,扩容至2倍
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length); // 扩容 4.1*
hash = (null != key) ? hash(key) : 0;
}
}
// 4.1*
void resize(int newCapacity) {
//...
Emtry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity)); // 迁移,rehash,有死锁问题 4.1.1
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
// 4.1.1*
void transfer(Entry[] newTable)
{
Entry[] src = table;
int newCapacity = newTable.length;
// 从OldTable里摘一个元素出来,然后放到NewTable中
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
Java1.7 HashMap存在的问题
- 并发环境中容易碰到死锁
参考:疫苗:JAVA HASHMAP的死循环
- 假设有两个线程,线程二执行完成rehash(前插):
- 线程一被调度回来执行:
- 环形链接出现
- 有潜在的安全隐患
可以通过精心构造的恶意请求引发DoS
String的hashCode()
方法是通过每个字符计算哈希值,所以容易产生哈希碰撞,例如 “Aa”, “BB”, “C#” 的哈希值都是2112。
所以1.7中的hash()
方法中先判断了如果是String,用另一种方法(不是String提供的hashCode)来计算哈希值,避免潜在攻击。
Java1.8 HashMap的改进
- 优化
hash()
方法
采用高16位异或低16位,避免低位相同高位不同的哈希值产生严重的碰撞。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 数组+链表/红黑树
- 扩容时插入顺序的改进
- 其他
- 函数方法:forEach, compute系列
- Map的新API:merge, replace