前言
Java hashmap 面试总是避不开会问为什么hashmap的哈希桶长度是2的幂,本文通过以下几个方面进行深入分析
- hashmap 创建与哈希桶长度分析
- hashmap 插入数据与哈希桶内存分配
- hashmap hash寻址与哈希桶长度分析
- hashmap 扩容与哈希桶内存重新分配
特别说明本文基于JDK1.8进行分析
预备知识
1. 十进制数2的整数幂用二进制如何表示?
通过一张图可以看出2的n次幂由十进制转为二进制的过程
从图中可以看到2的n次幂在二进制中最高位都为1其余位均为0,最高位以后有n个0,n就是十进制中的幂次。
2. 计算机中二进制与位运算有什么特殊关系?
众所周知计算机处理的数据都是二进制数据,二进制数据计算都是通过位运算来完成的。其中比较常见的位运算为 “且” “或” “异或” “左移” “右移”等
通过图中二进制数据不难发现当二进制有效位为1其余位为0时有效位的左移相当于扩大为原来二倍,右移相当于变为原来一半。
在二进制算法中存在一种快速取模运算算法
m%n 等价于 m&(n-1)成立的条件是n是2的整数次幂例如 7%16 等价于7&15 进行按位或运算,此算法就巧妙的应用到hash桶定位上
有了以上的背景知识后开始分析hashmap中2的n次幂到底有何好处
hashmap 创建与哈希桶长度分析
在hashmap创建时可以使用默认构造方法或带参数的构造方法
HashMap map1 = new HashMap();
HashMap map2 = new HashMap(65);
这两种方法不论哪种在内部创建哈希桶数组时都会将哈希桶的长度变为2的整数幂并且这个值时大于指定初始化容量的最小的整数幂
无参数的构造函数内部默认值为16,有参数的构造函数会通过算法转换如65会转为128
下面介绍具体转换算法
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;
}
以65为例其二进制 1000001,65-1=64 其二进制1000000
结果128正好时大于65的最小2的整数幂。
hashmap 插入数据与哈希桶内存分配
其实在构造函数创建完对象后并未真正的分配内存空间而是在第一次插入数据时进行哈希桶初始化而初始化哈希桶的长度就是通过上面介绍的方法确定。这其实也是一种优化避免初始化后长期没有插入数据而造成内存浪费。
hashmap hash寻址与哈希桶长度分析
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
hashmap在进行插入数据时根据key的hashcode通过hash()算法重新计算hashcode。此处肯定会问为什么有hashcode还要在进行一次hash呢?
其实就是为了加入hash混淆使hash冲突的概率更小,hash()函数其实就是把hashcode的高16位跟低十六位进行异或操作
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
计算完hash值后就是通过&位运算进行求模运算定位在哈希桶中的位置
hashmap 扩容与哈希桶内存重新分配
hashmap当容量达到一定阈值时会进行扩容阈值大小默认为0.75。扩容是发生在数据插入完以后。发生扩容后新的哈希桶数组会扩容位原来大小的2倍然后将旧hash桶中的节点移动到新哈希桶数组中。移动的主要逻辑:
- 如果原hash桶没有冲突链直接根据hash值与新桶数组长度-1后&操作定位新位置
- 如果存在冲突链且冲突链是链表则根据hash值与hash桶长度进行&操作(注意这里时跟hash桶长度也就是2的整数幂,这个二进制数有个特点就是有效位是1其余为0)&操作后结果要么为0要么还是原来的哈希桶长度,当等于0时新哈希桶数组下标与原下标相同,不等于0时新下标为原桶下标+原哈希数组长度作为新下标。
至此hashmap中2的整数幂跟位运算相关的逻辑已经全部讲完
总结
其实hashmap这么设计更多是为了效率,而计算机中位运算是最快的计算方式因此按照2的整数幂进行分配可以利用位运算的优势。
最后推荐一款作者自己开发的番茄钟小程序,支持番茄专注,翻页钟倒计时,日,周,月,年等维度信息统计。