Java1.7 HashMap源码解读

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()方法中:

  1. 如果表为空,则调用inflateTable()方法开辟空间,其中调用roundUpToPowerOf2()方法将容量上调至2的幂
  2. 调用hash()方法计算哈希值
  3. 调用indexFor()方法计算索引值(上述的按位与(&)运算)
  4. 遍历数组中的链表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存在的问题

  1. 假设有两个线程,线程二执行完成rehash(前插):
    并发下的rehash-1
  2. 线程一被调度回来执行:
    在这里插入图片描述
    在这里插入图片描述
  3. 环形链接出现
    在这里插入图片描述
  • 有潜在的安全隐患
    可以通过精心构造的恶意请求引发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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值