HashMap系列文章(传送门):
二:源码级理解HashMap之resize()方法,带你一行行手撕
三:源码级理解HashMap之putVal()方法,一行行手撕源码
四:源码级理解HashMap之get()和remove()方法
壹:HashMap的基本属性
//序列化ID
private static final long serialVersionUID = 362498820763181265L;
//哈希表默认的桶数,也就是数组的长度,同时这个容量必须是二的次数幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//桶的最大数量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树化的阈值
static final int TREEIFY_THRESHOLD = 8;
//从树化变回链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
//树化的另一个控制参数,最小的桶数
//所以树化:链表个数大于8,同时桶数不小于64
static final int MIN_TREEIFY_CAPACITY = 64;
//数组+链表中的数组
transient Node<K,V>[] table;
//threshold表示当HashMap的size大于threshold时会执行resize操作,就是扩容阈值
int threshold;
//当前key-value键值对个数
transient int size;
//结构修改时会自增,用于fast-fail机制
transient int modCount;
//节点内部类,Node相关属性和方法
static class Node<K,V> implements Map.Entry<K,V> {
//Node的hash值
final int hash;
//键值key
final K key;
//value值
V value;
//链表的next节点
Node<K,V> next;
//构造方法,很简单,过
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
//get,set,toString过
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
//计算hash值的算法,底层调用Object.hashCode()的native方法,
//说明底层由c++实现,这里就是返回key的哈希与value的哈希异或的值
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//设置新值,并返回老值
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
//重写equals方法
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
看到这里你可能会有几个疑惑?
这里会先给出结论,原理我们会在后文中一点点揭开面纱
1.为什么桶的长度必须是2的整数次幂?
- 在后文中查找hash地址的时候,会进行取模运算,如果利用位运算进行取模运算必须保证长度为2的整数次幂
2.table的默认值为null,到底是什么时候初始化的?
- 使用懒加载机制,当第一次put()时才会初始化,防止你new出来一个HashMap你不用,造成的内存浪费,在集合中懒加载机制十分常见
3.为什么要将链表树化?为什么又变回来?
- 当链表太长时,遍历又变成了链表的顺序查找,效率低下,变成红黑树可以加速查找速率(红黑树后期带你手撕),又变回来是因为链表长度缩短后,没必要使用红黑树,顺序查找足矣
4.什么是负载因子呢?
- 比如说当前的容器容量是16,负载因子是0.75,16*0.75=12,也就是说,当容量达到了12的时候就会进行扩容操作,也就是resize()方法,负载因子是根据时间和空间权衡下经大量实验得到的,所以一般我们不传入负载因子
贰:构造方法
- 双参构造器:指定HashMap的初始容量和负载因子
public HashMap(int initialCapacity, float loadFactor) {
//检测initialCapacity和loadFactor的合法性
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//给负载因子赋值
this.loadFactor = loadFactor;
//tableSizefor()方法稍后讲解
//这里就是根据传入的初始容量返回扩容阈值(必须是二的次数幂大小)
this.threshold = tableSizeFor(initialCapacity);
}
- 指定初始容量创建HashMap
套娃,调用双惨构造器,负载因子传入DEFAULT_LOAD_FACTOR默认负载因子
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
- 空参构造器
传入默认负载因子0.75,其他字段都是默认值
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
- 传入集合构造器
这里涉及到put操作,我们在后文中再来解释
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
叁:神奇的异或操作
tableSizeFor()
方法,根据传入容量返回一个>=cap
的最小二的整数次幂的数
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;
}
如何实现,这里结合一个栗子cap=10
画图给你解释:(这里基本的位运算就不解释了)
首先我们明白一点,2的整数次幂的数,最高位是1,其余位都是0,例如16的二进制位:10000
,那么15就是1111
,所以反复或的操作就是不断地把每个位数都变成1,最后+1就变成了最近的二的整数次幂的数
第一次或操作:向右移动一位,最高有效位1也会向右移动一位,我们知道或操作有1就变成1,所以最高两位有效位都会变成1
第二次或操作:当再向右移动两位,相当于把第一步的高两位向后移动两位,所以或运算后,四位都是1
此时已经完成操作了,再移动结果不改变,最多移动16位,是因为int
4个子节,32位,最多移动高16位到低16位就能完成操作
所以当位运算操作完之后,(n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
这里如果不超过最大值就返回n+1,例子中返回15+1 = 16,所以这就是sizeForTable()
的算法
- 再介绍后文中会使用到的位运算:取模运算
(n - 1) & hash
常用的hash函数有:直接定址,数字分析,等等(数据结构应该都学过),HashMap中使用的是初留取余法,就是取模运算,再HashMap底层使用了位运算实现取模操作,即(n - 1) & hash = hash % n
,n表示桶也就是数组的长度
位运算实现取模操作有个前提,n必须是二的整数次幂,也就回答了上文的问题
同样,我用一个栗子结合图告诉你原理:a%b=a&(b-1)
假设a=10,b=8
所以我们想取余数,就是得到移出来的三位,也就是原来1010的后三位,所以接下来求后三位,首先我们先知道一个特性:一个数和1做与运算都是本身:0&1=0,1&1=1,所以我们将b,也就是一个n位2的整数次幂的数-1就会得到n-1位全1的数,例如16:10000,15:1111
结合这两个特性,就可以得到余数
- Hash()算法中的位运算
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里hash函数根据key获取散列地址,h = key.hashCode()) ^ (h >>> 16
操作意义在哪?
上面的方法中,我们可以看到hash值会和长度做位运算,当数组的长度很短时,只有低位数的hashcode值能参与运算。而让高16位参与运算可以更好的均匀散列,减少碰撞,进一步降低hash冲突的几率。并且使得高16位和低16位的信息都被保留了。
大家可以打开hashmap的源码,找到hash方法,按住ctrl点击方法里的hashcode,跳转到Object类,然后可以看到hashcode的数据类型是int。int为4个字节,1个字节8个比特位,就是32个比特位,所以16是因为32对半的结果,也就是让高的那一半也来参与运算