HashMap 的一种数据集合类型,它存储的是键值对,jdk1.8前后hashmap是有所改变的,
1.8之前用的数据结构是数组 + 链表,1.8之后用的数据结构是数组 + 链表 + 红黑树,红黑树的引入是为了提高它的查询效率,链表的查询效率是O(n),而红黑树的查询效率是O(logn),
1.8之前如果发生哈希碰撞采用的是头插法,1.8之后发生哈希碰撞采用尾插法,因为头插法在多线程的情况下会出现循环链表。由于HashMap线程不安全的,一般在多线程的情况下一般采用线程安全的 CurrentHashMap
1.8前后还有一些优化的细节,记得可能不太清了,接下来我根据1.8的HashMap源码,从他的构造方法、PUT、GET这三个方法来说一下HashMap的基本原理。
构造方法
在创建HashMap的时候,根据阿里规约,我们都会传入一个初始容量,这个初始化容量最好是2的次幂。即使传入的不是2的次幂,他也会通过tableSizeFor方法,把它转化成大于等于它的最小二次幂。
PUT
PUT方法需要传入(key,value),里面会调用putVal方法,该方法需要指定三个参数,key,value和hash(key)
这个hash(key)的计算方法如下:key的哈希code值 ^ key的哈希code值无符号右移16位(这样计算的原因:保证最终获取的存储位置尽量分布均匀),这个方法也表明key值可以为null,hash(null)= 0;
putVal 方法会判断:
1、数组是否存在,如果不存在,会调用resize() 方法(这个也是扩容的方法)
2、传入的哈希值通过和(数组容量 - 1)进行与运算,得出分配数组空间下标,判断该空间是否为null,如果为null,就把该键值对存入到数组中(这也是为什么容量要设置为2的次幂。因为与运算的效率要比取余运算高。)
3、如果不满足以上两个判断,再去遍历链表或树去判断是否有重复值,如果有就直接覆盖,如果没有就添加。
在添加键值对时,可能会出现扩容和树化,这两个过程都是很耗性能的操作,当然会有条件:
扩容的条件:size >= 容量 * 加载因子 以2的倍数扩容
树化的条件:容量 >= 64 && 链表长度 >= 8
树化的过程:把普通节点转化为树节点,在调用 treeify 进行树化。
GET
GET方法需要传入一个key值,里面会调用getNode(hash(key),key)方法,返回key值相等的节点
getNode方法会判断:
如果在数组中,则直接返回,
如果在链表或树中,则需遍历方法去查找。