1、什么是HashMap
HashMap是基于hash表的一个Map接口实现,数组+链表的存储方式
数组的特点:查询快,新增和删除慢
链表的特点:增删快,查询慢
数组+链表将两者的特点结合使用
如下图:
其中每个tab[x] 都是一个node
Node:是一个内部类
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; ... }
字段的理解:hash(根据key获取的hash值);key(put数据的key); value(put数据的value)
next(链表中的下一个node)
2、HashMap做什么用?
HashMap是用于存储key-value键值对的集合,用于存储和获取对象数据
3、HashMap 怎么用
常用方法:put和get,remove
1、创建HashMap对象:Map<String,Object> map = new HashMap<>();
创建对象,需要注意几个参数:
capacity(参考容量);threshold(扩容阈值);loadFactor(加载因子)
capacity(参考容量)
用来作为Map中Node[]数组创建的默认长度,默认长度16,可以在创建时自己定义capacity值;Map<String,Object> map = new HashMap<>(capacity);
但是hashMap内部有一个机制:创建map对象中的Node[]数组的初始长度必须要是2的n次方,当你设置长度是23的时候,HashMap会把初始化长度设置为32,因为23在16(2的4次方)到32(2的5次方)之间,取最大数32
threshold(扩容阈值)和 loadFactor(加载因子)
hashMap在新增数据的过程中达到扩容阈值,Node[]数组长度进行扩容
默认阈值:参考容量*加载因子
加载因子默认0.75f,可以修改不建议修改
2、首次put数据:map.put("testOne","testOne值”)
(1)获取“testOne”的hash值
int hash = hash("testOne");
(2)判断数组是否为空
判断Node[]数组是否为空,如果为空,创建初始化长度的数组空间
(3)对数组长度做减1和hash值做与运算得数组下标
直接算出来的hash值可能非常大,不可能直接当作数组下标的,对此hashMap的设计者有自己的解决方案:求余
也就是:index = hash值%数组长度
这样index的值永远都在数组长度之内,也就可以作为数组下标了,但是这样做有一个缺点:效率低,于是hashMap的设计者把求余改成了一个效率更高的运算:数组长度减一和hash值与运算
也就是:index = hash值&(数组长度-1)
为什么这样得出来的index也在数组长度之内呢?可以看下例子(由于是位运算,需要把hash值和数组长度分解成二进制,这样看的更清楚,假设它两二进制只有八位):
数组长度: 0001 0000
数组长度-1: 0000 1111
hash值: 1101 0101
与操作: 0000 0101
可以看到,数组长度-1后,前四位变成了0,跟hash值作与操作之后,hash值前四位不管是什么,都会变成0,而后四位由于对方都是1,因此得以保留下来。这样得到最后的结果永远都不会超过数组长度。
这里必须满足一个前提条件:数组的长度必须是2的n次方,这样长度减一,最前面的二进制数组会从1变为 0,与运算之后长度一定在数组长度之内
(4)把值赋给对应的node
数组下标拿到了,要插入的位置也就基本确定了。在插入之前,hashMap会去判断当前数组位置上没有元素,由于我们这是第一次插入,因此这里就是直接插入元素
这里插入的方式很简单,就是把node的四大参数(hash值、key、value、next)赋给当前数组位置上的node。由于是位置上第一个元素,后继没有链表元素,next的值就是null。
(5)插入数组之后操作
插入之后,hashmap的全局变量:size,也就是已有元素数量,加一,然后看下有没有大于扩容阈值,如果大的话就要扩容。
(6)效果如图(这里假设“testOne”获取的下标是2):
3、 put不同key的情况:map.put("testTwo","testTwo不同的key")
流程如上2,效果图如下:
4、put相同key的情况:map.put("testOne","testOne另外一个值”)
得到下标数据只之后,发现位置上已经有了元素数据这种情况叫做:hash碰撞
这就要判断此位置上的元素是不是同一个key,是的话就替换,不是的话就追加到后续链表-这就是链表的作用
(1)hash值是否相等
首先看一下算出的hash值是否相等,这里算出的是相等的
(2)key是否相等
hash相等,两个key不一定相等,因为我们算hash值需要调用hashCode()方法,可以hashCode()方法我们是可以重写,这样就有可能出现相等的hash值
这里就需要对两个key进行=和equals比较, 此比较结果是相等的,覆盖原值
为什么是先判断hash值是否相等,后判断的key:性能
hash不相等的,一定不是同一个key;两个key相等,hash值一定相等
效果图如下:
5、首次put不同key值但index下标相等:map.put("testThree","testThree的值”)
假设"testOne"和"testThree"获取到的index下标是相同的
在此前提下,会出现hash碰撞,逻辑如4,不同之处在于在判断两个key时,两个key的hash值是不相等的,两个key自然也不是同一个key
因为是首次,所以"testOne"链表的.next是null,直接插入到“testOne”的后面成为.next;"testThree"的.next赋值为null
效果图如下:
6、 继续put不同key但index下标相等:map.put("testFour","testFour的值”)
假设“testOne”和“testFour”算出来的index下标相同
流程和5相同,不同的是遍历链表,判断hash和key,如果相等直接赋值
效果如下图:
7、put数据时链表长度大于等于8的情况(扩容或转为红黑树):
map.put("testEight","testEight的值”)
假设"testEight"和"testOne"算出的index下标是一致的
假设“testOne”的链表长度达到了7
注:随着put的数据越来越多,hash碰撞的频率也越来越高,会造成链表长度越来越长,这样每次put数据遍历的时间也越来越长,使hashmap的性能越来越差,怎么提高性能:扩容或者链表转红黑树
(1)扩容机制
扩容是增加数值长度,减少hash碰撞的频率,从而提高性能.
当链表长度大于等于8时,会判断数值长度是否小于64,如果小于,执行
resize();进行扩容
(2)链表转红黑树
为什么链表达到一定长度(8)要转为红黑树:
-------新开一遍专门讲解---------
什么是红黑树:红黑树是一种特殊的平衡二叉树,平衡二叉树具备的特征是:二叉树左子树和二叉树右子树的高度差的绝对值不超过1。
putVal代码如下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断Node[]数组是否为null,如果是,执行扩容
if ((tab = table) == null || (n = tab.length) == 0)
//扩容
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null) //取Node[]数组长度减一和hash值进行与 运算,得数组下标,判断此数组下标里的元素值是否为null,如果是null直接赋值
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
//如果此下标下的元素值不为null,判断两hash和两key是否相等,如果相等,
替换原来的元素值
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果不等,遍历链表
for (int binCount = 0; ; ++binCount) {
//如果此链表的下一个元素(next)是否为null,如果是null,直接赋值next,
新的元素next设置为null
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果链表长度大于等于8,进行扩容或转为红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果此链表的下一个元素(next)不为null,比较两hash和两key,如果都
相等,直接赋值
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; //如果不能继续循环
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); //赋值
return oldValue;
}
}
++modCount;
//判断数组内元素个数是否大于扩容阈值,如果大于,执行扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
8、resize()数组扩容
总结:
(1)先计算出对应key的hash值,然后去判断当前Node[]数组是不是为空,为空就新建,不为空就对hash值作减一与运算得到数组下标
(2)然后会判断当前数组位置有没有元素,没有的话就把值插到当前位置,有的话就说明遇到了哈希碰撞
(3)遇到哈希碰撞后,就会看下当前链表是不是以红黑树的方式存储,是的话,就会遍历红黑树,看有没有相同key的元素,有就覆盖,没有就执行红黑树插入
(4)如果是普通链表,则按普通链表的方式遍历链表的元素,判断是不是同一个key,是的话就覆盖,不是的话就追加到后面去
(5)当链表上的元素达到8个的时候,如果不满足扩容条件,链表会转换成红黑树;如果满足扩容条件,则hashmap会进行扩容,把容量扩大到以前的两倍
参考:HashMap put原理详解(基于jdk1.8)_Python研究所-CSDN博客