通过Hash表实现的一个Map结构,下面通过它的三个主要方法介绍一些技术细节。
get
get比较简单,就是通过 key 返回对应的 value。
那么如何获取key所在下标呢?
- 首先计算 key 的 hashcode;
- 将 hashcode 右移16位与自己异或得到 h;
- h & (length - 1),length是当前 hashtable 的长度,就得到了 key 在 hash 表中的对应的下标
下面详细说下细节:
-
h & (length - 1)是什么意思?
如果length为2的幂次,&(length-1) 和 %length 得到的结果是一样的。而位运算效率更高,因此这也是选择表长度为2的幂次的好处之一。
-
为什么右移16位异或?
这样操作之后,低16位叠加上了高16位的影响。
通常来说 hash 表比较小(小于2^16),如果直接拿 key 的 hashcode 取余求下标,那么高16位将不起作用,这样的话如果两个 key 的 hashcode 高位不同,但低位碰巧相同,就会散列到一个桶内。这样做就是避免高位不参与散列作用,减少散列冲突。
put
这个也比较简单。通过 key 计算出存储的下标,如果没有元素直接存进去即可,如果有则对比下key是否相同,相同则更新value。如果是链表或者树,遍历或搜索树即可。
resize
当表中元素达到一定阈值(75%的表长度)时,为了避免频繁hash冲突,将表扩容(扩大至两倍)。
-
细节问题在于如何将旧表元素移到新表中去?
上面说到,计算存储位置时是使用 h&(table.length-1) 来计算的。当我们移到新表的时候,在同样的通过这个方法计算一下下标。
旧表length-1=15 1 1 1 1 key 计算出来的h a b c d e 新表length-1=31 1 1 1 1 1
可以看到第5位a新参与了运算。如果这位为0,那么和旧表计算得到的结果是一样的,这个元素迁移到新表中下标还是不变;如果是1,那么就恰好会多比原来的值大一个oldtable.length,即产生了一个很好的偏移。因为a是由hash值计算得到的,具有很好的随机性,这样每个键值对要么在左边这一半,要么在右边那一半,拆分均匀。仅仅是利用了hash的随机性,没有增加任何其他的操作,实现非常的优美。
-
对于链表来说如何移动?
链表也是会根据每个节点的 hashcode 计算,然后就可以拆分成两个链表,一个偏移,一个不偏移。树也是一样的,树的节点在内部其实是串成一条链表的,按链表进行处理,再根据节点数量看是否要转换成树。