HashMap学习

Samlen_Tsoi

在讨论HashMap之前,首先抛出以下几个问题:

  1. 为什么HashMap的容量总要保持为2的幂次方?
  2. HashMap在什么条件下会进行扩容?
  3. HashMap在什么条件下会进行树化?

带着这上面几个问题,我们进入HashMap的探讨。

在JDK1.7及之前,HashMap是由数组+链表所组成,而到了JDK1.8之后,加入了红黑树。
image.png
当我们向HashMap中put的一个k:v数据之后,它会找到一个合适的数组下标,把v存放进去。那么HashMap是如何确定数据存放的数组下标?

如果让我们来设计,我们如何设计呢?首先数组的下标必然是一个整数,这时我们可以联想到Java的所有对象中都会有一个整型hashCode,直接使用hashCode作为下标当然是行不通的,因为会存在数组越界的问题。那么是否存在某种算法f(hashCode)得到值的范围刚好是在0~(length-1)内(lengh为数组的长度),这时我们就会想到取模运算hashCode % lengh,这样就可以确保得到的数组下标不会越界。
事实上,HashMap的思路也是这样,但它做了两点改动:

  • 不是通过取模运算得到下标,而是通过(length - 1) & hash计算出下标。通过&运算同样可以得到0~(length-1)的结果范围。为什么这么说呢,可以看下面的分析。
length:   0000 0000 0000 0000 0000 0000 0001 0000
length-1: 0000 0000 0000 0000 0000 0000 0000 1111
                             &
hash:     0000 0000 0000 0000 0000 0000 0101 1011

假如length=16,那么length-1=15,由于&运算是位运算,15的二进制的低四位全为1,其余高位都为0,当它与其它二进制进行&运算的时候,只有低四位有效,因此得出的范围是:0000-1111,转换为十进制即:0-15。之所以选择&运算计算下标,是为了计算效率。这时候,你会发现hash似乎只有低四位参与了运算,其余高位似乎没什么用,这样可能会导致散列的效果不好。接着看下面的分析你就会明白。

  • 不直接取k的hashCode,而是通过hashCode进行异或预算得到新的hash值,这样做的目的是为了让高位也参与到第一点说的&运算中去。
static final int hash(Object key) {
 int h;
 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
h = key:0001 0011 0111 0111 1000 1100 1111 0011
                            ^
h>>>16:  0000 0000 0000 0000 0001 0011 0111 0111

最终得到的hash的低16位其实是原来hashCode的低16位与高16位进行^运算得到的,因此在进行(length - 1) & hash计算下标的时候,hash的高位也参与进来了,这样便更好的散列。

现在再次回到(length - 1) & hash的计算逻辑,如果length不是2的幂次方会出现什么情况?假如length现在是17,那么length-1即为16,那么得出来的二进制数要么是00000要么是10000,这样散列的效果可想而知。这就可以解释开头提出的第一个问题:为什么HashMap的容量总要保持为2的幂次方?,但这只是原因之一。

length-1: 0000 0000 0000 0000 0000 0000 0001 0000
                             &
hash:     0000 0000 0000 0000 0000 0000 0101 1011
阅读 112
0 声望
0 粉丝
0 条评论
0 声望
0 粉丝
宣传栏