HashMap源码分析

扰动函数

为什么使用扰动函数

增加随机性,让元素散列均匀,减少碰撞。

源码分析

看下hashMap计算hash的源码:

/**
 * Computes key.hashCode() and spreads (XORs) higher bits of hash
 * to lower.  Because the table uses power-of-two masking, sets of
 * hashes that vary only in bits above the current mask will
 * always collide. (Among known examples are sets of Float keys
 * holding consecutive whole numbers in small tables.)  So we
 * apply a transform that spreads the impact of higher bits
 * downward. There is a tradeoff between speed, utility, and
 * quality of bit-spreading. Because many common sets of hashes
 * are already reasonably distributed (so don't benefit from
 * spreading), and because we use trees to handle large sets of
 * collisions in bins, we just XOR some shifted bits in the
 * cheapest possible way to reduce systematic lossage, as well as
 * to incorporate impact of the highest bits that would otherwise
 * never be used in index calculations because of table bounds.
 */
static final int hash(Object key) {
   
   
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

可以看到,计算hash时,使用hash与右移16位的hash做了异或运算。16位正好是自己二进制长度的一半,之后与原hash的做异或运算,这样就混合了原hash中的高位和低位,增大了随机性。
然后再用这个二进制数字与map容量减一进行与运算,就得到了这个key应该存放的位置。

初始化容量

先说总则:初始化容量只能是2的n次幂,如果声明不是,则自动转换为大于声明容量的最小的2的n次幂。
先看源码:

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and load factor.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
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);
}

可以看到在初始化容量时,如果容量不小于0并且没有达到最大容量,则调用tableSizeFor()方法。
tableSizeFor()源码如下:

/**
  * Returns a power of two size for the given target capacity.
  */
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;
}

|= 运算符相当于“或等”,即两个数某一位有一个是1即为1。所以这一串操作下来,其实是把传入的容量cap,变成了111……,正是大于cap的最小的2的几次幂-1,最后返回n+1,就正好是2的几次幂了。
比如传入的是17,二进制为10001,依次的执行结果如下:
int n = cap - 1 = 10000
n |= n >>> 1 = 11000
n |= n >>> 2 = 11110
n |= n >>> 4 = 11111
n |= n >>> 8; //不需要
n |= n >>> 16; //不需要
这样就得到了11111,即31,最后返回n + 1即32。
为什么一定要是2的n次幂呢?
这就与上面的扰动函数关联起来了。2的n次幂减一正好是11111……这样的形式,与扰动函数的hash进行与运算,可以使散列更加均匀,减少碰撞。

负载因子

/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

这是HashMap默认的负载因子,0.75,当使用容量达到75%时,map就会自动扩容。
通过上面的构造函数可以看出来,这个负载因子我们也可以通过构造函数在创建map的时候传进去。
负载因子越小,就越不容易产生碰撞,map的性能也就越好。所以如果希望用空间换时间,可以把负载因子设置的小一些。
map扩容大小为原来的二倍newCap = oldCap << 1

扩容元素拆分

map进行扩容后,原来的元素就要拆分到新的map中。JDK1.7时,需要重新计算hash值,比较费时。而JDK8中进行了优化,不再需要重新计算hash值了。
那么JDK8是如何进行拆分的呢?

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;
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Counter-Strike大牛

创作不易,感谢鼓励。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值