一、HashMap初始化原理
常规的初始化HashMap就是直接new,那么new之后到底底层做了哪些工作呢,接下来将详细展开讲解。
- 首先new HashMap(),然后给一个默认的负载因子,就结了。
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
- 然后进行put值,这时候就要思考了,put值的时候,数据是怎么存放的,我们知道它是键值对。
那使用什么样的数据结构存放的键值对呢?
1. Node的介绍
- Node来存储键值对,那么Node的结构一定包含key、value了。
- 既然是HashMap,那一定也要有hash值,所以Node里面还需要存放hash值。
- 我们知道HashMap的底层结构是数组+链表+红黑树,那么当hash冲突的时候,就需要链化或者树化,就需要记录下一个节点的位置,所以Node中会有next。
所以HashMap是用Node存储的键值对。
2. put初始值
上面我们知道了Node来存储键值对,HashMap的结构是数组+链表+红黑树,那么就要Node[]来存储各个Node。put的话,肯定是要知道put到了数组中的哪个位置的。
现在我们初始化值,先思考new HashMap,然后put一个值的话,什么逻辑?
答:既然之前是new的空map,那么就需要先给map分配空间,然后计算put值的索引位置,放到对应位置里面就行了。
那要是put多个值呢?
答:同样分配空间,计算put值的索引位置,判断是否位置冲突,如果冲突了那么应该如何解决?
下面就详细介绍下put一个值的过程。
按照常规开发逻辑,我们一定是要先判断HashMap是否为空的,这里用Node[]来存储各个Node值。
- 判断Node[]是否为空,长度是否为0(我们初始化的空map,所以满足条件)
- 确定是空map,分配空间resize()
- 设置Node[]长度是初始值,默认长度1<<4,即16
- 设置初始阈值,用来判断是否扩容的,负载因子初始长度,即0.7516
- 空间分配好了,new Node[16],长度16
- 计算put值的索引位置,计算方式是(n-1)&hash
- 比较位置是否已经存在值,我们的情况不存在,那么就可以给node赋值了
- node[i] = newNode(hash,key,value,null)
以上就是new一个空map之后,插入一个值的底层方法。是不是也不复杂~~~~
下面详细介绍一下计算索引位置的方法。
3. 索引位置的计算原理
代码中使用的计算公式是index=(n-1)&hash,n是数组的长度,hash是插入值的hash值
是位运算,我们能想到的第一个好处就是快,二进制效率肯定杠杠滴!!
那为什么这样算呢?
首先数组的长度都是2的次幂,那以初始化的长度举例计算一下,初始化长度是16
16:0001 0000
15:0000 1111(n-1=16-1=15)
那分别&hash值,&算法是全为1值为1,其他都是0
接下来分别例举
16&hash
16 :0001 0000
hash :0000 1010
& :0000 0000
16 :0001 0000
hash :0001 1010
& :0001 0000
16&hash值,可以看出,只能出现两种情况,一个是16,一个是0,很明显,这样值分布的是非常不均匀的,会出现大量哈希冲突问题。
15&hash
15 :0000 1111
hash :0000 1010
& :0000 1010
15&hash,可以看出,低四位都是1,那么&hash的结果,就是hash的低四位结果。那么值的范围就是0~15,相比之下,15&hash的计算结果一定是分布的更均匀的。
这里另提一点(n-1)&hash=hash%n,但是为什么不用取模呢,我想大概是因为效率原因吧。
二、手写HashMap初始化和简单赋值
- 初始化代码
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
- 赋值
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
// 判断数组是否为空,如果是空的,就进行扩容,给n赋值数组长度
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 判断值的索引位置是否为空,为空证明可以直接放进入值;
// 如果不为空就哈希冲突了, 目前的场景不存在这种情况
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
// 记录元素增减次数
++modCount;
return null;
}
- 扩容
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
}
else if (oldThr > 0) // initial capacity was placed in threshold
else { // zero initial threshold signifies using defaults
// 上面情况不存在,目前就是初始化数组容量和阈值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
threshold = newThr;
// 初始化数组空间
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
return newTab;
}
三、为什么HashMap数组长度一定是2的次幂
n=17,16&hash
16 :0001 0000
hash :0000 1010
& :0000 0000
16 :0001 0000
hash :0001 1010
& :0001 0000
n=16,15&hash
15 :0000 1111
hash :0000 1010
& :0000 1010
结论上面说过,就不复述了。
总之,HashMap 的数组长度是2的次幂,是为了提高性能、简化操作、减少哈希冲突,并在扩容时保持高效的索引计算。