Java1.8中HashMap的三个细节

0 写在前面的

在写这篇文章之前先把Java中HashMap的作者列出来,向他们致敬!

/*
* @author  Doug Lea
* @author  Josh Bloch
* @author  Arthur van Hoff
* @author  Neal Gafter
* @see     Object#hashCode()
* @see     Collection
* @see     Map
* @see     TreeMap
* @see     Hashtable
* @since   1.2
*/

Doug Lea,Java util.concurrent包的作者。
Josh Bloch,Java 集合框架创办人,Effective Java的作者。
Arthur van Hoff,据说Java命名来源于开发人员名字的组合:James Gosling、Arthur Van Hoff和Andy Bechtolsheim首字母的缩写。
Neal Gafter,Google的软件工程师和Java的传道者。

1 频繁出现的位赋值操作

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final int MAXIMUM_CAPACITY = 1 << 30;

这是一个及其有参考意义的操作,初学者往往会直接给初始容量赋16。我们知道计算机只能记录二进制数,所以1<<4这种对位直接进行的操作对计算机来说更加容易处理,不然的话计算机需要将16先转成10000再进行存储。

2 神奇的阈值和默认链表长度

众所周知,HashMap有一个默认的阈值和链表长度,在默认状态下,他是这样设置的。

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;   
    /**
     * Constructs an empty <tt>HashMap</tt> with the default initial capacity
     * (16) and the default load factor (0.75).
     */
    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

可能大家不太理解为什么这里的初始阈值设置为0.75以及链表的最大长度设置为8,很多资料上说是为了达到时间和空间的平衡,使插入、查询的效率最高。这需要从泊松分布说起。

2.1泊松分布

泊松分布(Poisson distribution),是一种统计与概率学里常见到的离散机率分布(discrete probability distribution)。泊松分布的概率函数为:

2.2概率问题 

源码中解释到,当λ的均值为0.5,阈值为0.75时,在忽略方差的情况下,k的预期出现次数是exp(-0.5) * pow(0.5, k) /factorial(k),他的首值为:

     /*
     * 1:    0.30326533
     * 2:    0.07581633
     * 3:    0.01263606
     * 4:    0.00157952
     * 5:    0.00015795
     * 6:    0.00001316
     * 7:    0.00000094
     * 8:    0.00000006
     */

我们看到当阈值为0.75,如果要重复出现8次哈希冲突的概率为亿分之八,不足千万分之一。
现在我们就可以理解设计者的苦心了,0.75、8这些数字不是凭空想象出来的,而是靠很多数学理论支撑的!

3 更高效的取模运算(2的幂次)

关于哈希函数的原理,很多文章讲解地非常清楚,不再赘。只是想就一句话进行分析。

  if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null);

Java是怎么把一个32位的int型哈希值均匀地散列到我们的一个大小为16的数组(暂时不考虑扩容)里的呢?一个常规的思路是这样的:

int i = hash % 16;

这样写完全没有问题,且计算的效率很低。

从源码里面我们就可以看出端倪来,只是简单的一步(n - 1) & hash。这里的n为数组长度,实现的过程如下,这里假设长度为16。16的二进制为0x10000,这个二进制减一就是把首位置零,后面的位全部置一,这里是0x1111=15。将减了1的0x1111和哈希值做与运算,我们知道不管是1或者0和0做与运算后都是0,不管是1或者0和1做与运算后都是它本身。通过这个运算,我们就可以把这个哈希值的后四位取出来了!且这个数恰好是小于等于我们的数组长度的,真是妙不可言,需要多次运算的%在这里通过一步按位运算就可以解决了。

这也是为什么我们的HashMap的容量都为2的幂次的一个关键原因。

4 简化了的哈希运算

下面是1.7和1.8中哈希函数的对比,不是很明白为什么在1.8中减少了扰动的次数,可能是设计者觉得没有必要进行这么多次的异或运算,想要提高HashMap的效率吧

      // JDK 1.7实现:将 键key 转换成 哈希码(hash值)操作  = 使用hashCode() + 4次位运算 + 5次异或运算(9次扰动)
      static final int hash(int h) {
        h ^= k.hashCode(); 
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
     }

      // JDK 1.8实现:将 键key 转换成 哈希码(hash值)操作 = 使用hashCode() + 1次位运算 + 1次异或运算(2次扰动)
      // 1. 取hashCode值: h = key.hashCode() 
      // 2. 高位参与低位的运算:h ^ (h >>> 16)  
      static final int hash(Object key) {
           int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
            // a. 当key = null时,hash值 = 0,所以HashMap的key 可为null      
            // 注:对比HashTable,HashTable对key直接hashCode(),若key为null时,会抛出异常,所以HashTable的key不可为null
            // b. 当key ≠ null时,则通过先计算出 key的 hashCode()(记为h),然后 对哈希码进行 扰动处理: 按位 异或(^) 哈希码自身右移16位后的二进制
     }

之所以进行扰动是为了充分利用对象的哈希值,同时也可以有效地避免过多的哈希冲突

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值