为什么使用HashMap
- .HashMap是数组加链表结构(散列桶),存储的内容是键值对(key-value)映射
- HashMap采用是的数组加链表,可以在查询和修改方面继承了数组的线性查找和链表的寻址查找
- HashMap是非synchronized,效率快
- HashMap可以接受空的键值对(equlas方法需要对象)
HashMap中的负载因子和容量
实际容量 = 负载因子 x 容量,也就是 12 = 0.75 x 16。
负载因子为什么默认为0.75,官方文档解释 :
通常,默认的加载因子(0.75)在时间和空间成本之间提供了一个很好的方案。较高的值会减少空间的开销,但会增加查找的成本(在HashMap类的大多数操作中都能得到体现,包括最常用的( get() 操作和 put() 操作)。在设置映射表的初始容量的时候,应该考虑映射中预期的 Entry 数及其负载因子,以最大程度地减少 rehash(重新哈希)操作的次数。如果初始容量大于最大条目数 / 负载因子 ,则将不会进行任何哈希操作
如果要将许多映射存储到HashMap实例中,则创建具有足够大容量的映射将比使它根据需要增长表的自动重新哈希处理更有效地存储映射
HashMap工作原理
基于hashing,使用put存储到hashMap中,使用get从hashMap获取对象,当使用put传递键和值时,先对键调用hashCode方法,计算并返回hashCode值,用于找到Map数组中的bucket位置来存储对象。HashMap是在bucket中存储键值对象,作为Map.Node
put过程
- 对Key计算hashcode值,再计算下标
- 如果没有产生hash碰撞,直接放入bucket中
- 如果产生碰撞,以相同hashcode值为key,以链表的形式链接到后面。
- 如果链表长度超过阈值,把链表转成红黑树(java8以后,阈值为8),链表长度小于6,把红黑树转回链表
- 如果节点已经存在,替换旧值
- 如果桶满了(容量默认16*加载因子0.75),需要扩容resize();(扩容两倍,重新排序)
贴resize()源码
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//判断Node的长度,如果不为零
if (oldCap > 0) {
//判断当前Node的长度,如果当前长度超过 MAXIMUM_CAPACITY(最大容量值)
if (oldCap >= MAXIMUM_CAPACITY) {
//新增阀值为 Integer.MAX_VALUE
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果小于这个 MAXIMUM_CAPACITY(最大容量值),并且大于 DEFAULT_INITIAL_CAPACITY (默认16)
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
//进行2倍扩容
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//指定新增阀值
newCap = oldThr;
//如果数组为空
else { // zero initial threshold signifies using defaults
//使用默认的加载因子(0.75)
newCap = DEFAULT_INITIAL_CAPACITY;
//新增的阀值也就为 16 * 0.75 = 12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
//按照给定的初始大小计算扩容后的新增阀值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//扩容后的新增阀值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//扩容后的Node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果数组不为空,将原数组中的元素放入扩容后的数组中
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果节点为空,则直接计算在新数组中的位置,放入即可
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//拆分树节点
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//如果节点不为空,且为单链表,则将原数组中单链表元素进行拆分
Node<K,V> loHead = null, loTail = null;//保存在原有索引的链表
Node<K,V> hiHead = null, hiTail = null;//保存在新索引的链表
Node<K,V> next;
do {
next = e.next;
//哈希值和原数组长度进行&操作,为0则在原数组的索引位置
//非0则在原数组索引位置+原数组长度的新位置
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
get过程
当调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,再调用keys.equals()方法找到正确节点
有什么方法可以减少碰撞
扰动函数,hashmap内部算法实现,尽量返回不同的hashcode值,就会减少equals方法的调用,提升性能
//java8底层代码
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// ^ :异或 :https://blog.youkuaiyun.com/hello_cmy/article/details/81103042
// >>> 表示不带符号向右移动二进制数,移动16位,移动后前面统统补0;
// >>> :https://blog.youkuaiyun.com/yuqilin520/article/details/82886969
//为了避免出现相同hashcode值,减少碰撞
}
顺便记录异或
a^b
a==b --> 0
a!=b -->1
例如
7^6
7转化二进制位111 ; 6转化二进制位110
结果 : 001
所以 7^6=1
1^6=7
得出结论:一个数据异或另一个数据两次,最后得到的结果还是这个数据,用公式表示就是a ^ b ^ b=a。
部分资料参考:https://blog.youkuaiyun.com/Woo_home/article/details/103146845#commentsedit