HashMap简介
HashMap 基于哈希表的 Map 接口实现,是以 key-value 存储形式存在,即主要用来存放键值对。HashMap 的实现不是同步的,这意味着它不是线程安全的。它的 key、value 都可以为 null,此外,HashMap 中的映射不是有序的。
jdk1.8 之前 HashMap 由 数组 + 链表 组成,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突(两个对象调用的 hashCode 方法计算的哈希值经哈希函数算出来的地址被别的元素占用)而存在的(“拉链法”,数组储存的是每个链表的头结点,在下面称数组的单个元素为桶)。jdk1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(或者红黑树的边界值,默认为 8 )并且当前数组的长度大于 64 时,此时此索引位置上的所有数据改为使用红黑树存储。
为什么要引入红黑树?
哈希表的最优查找时间复杂度为O(1)。当链表过长时,时间复杂度就会转化为O(n)。所以当链表过长时,就要转化为红黑树,这样查找的时间复杂度为O(log n)
部分成员变量分析
默认的初始化容量,必须是2的n次幂
最大容量
默认的负载因子
桶中的链表元素大于这个值且数组的长度大于64时会转化为红黑树结构
当桶中的链表元素小于这个值时会转化回链表结构
在介绍中所说的数组,是HashMap的主体
HashMap中存在的元素个数
对HashMap的操作次数
阈值,即实际上HashMap中能达到的最大容量。为 capacity * load factor。
当HashMap中元素数量到达这个数量时,HashMap会进行扩容(数组大小变为原来的两倍)
负载因子,默认为0.75
值得注意的几个方法
hash()
为什么不用key本身的hashcode方法,而又处理了一下。当table很小的时候,计算元素在table中的位置(n-1)&hash(在put方法中利用这条式子求该key在HashMap的table中对应的下标),只用到了hash值的低位,这样当不同的hash值低位相同,高位不同的时候会产生冲突。实际上的hash值将hashcode低16位与高16位做异或运算,相当于混合了高位和低位,增加了随机性
tableSizeFor()
它在HashMap的给定初始化容量的构造函数中调用,将用户给定的容量变为至少大于它的2的n次幂。
为什么要变为2的n次幂呢?
计算元素在table中的位置(n-1)&hash,这里用的是&运算,其实我们也可以使用%运算,但是&运算比%运算快很多。于是为了替代hash%(n-1),这里就要求必须是2的n次幂。
构造函数
1.无参构造
2.包含别的HashMap来进行构造
3.指定初始容量
4.指定初始容量与负载因子
这里的
this.threshold = tableSizeFor(initialCapacity);我也搞不懂为什么。。。/(ㄒoㄒ)/~~
put()
put的流程.putVal()过长,可以自行去查看相关源码。
Jdk1.8中链表新元素添加到链表的尾结点
get()
get()方法较为简单,通过key的hashcode寻址到数组下标,若数组元素中的key和hash符合条件就会返回。否则遍历红黑树或者链表寻找。找不到则返回null
remove()
removeNode()过长,这里就不粘贴了。
根据key和hash找到结点,并将其去除。如果是红黑树,结点数小于6时转化回链表
resize()
resize()代码过长,这里就不粘贴了。
当HashMap中的size >= threshold 时(threshold=capacity * load factory)
就会触发扩容机制。
扩容的规则是这样的,因为table数组长度必须是2的次方数,扩容其实每次都是按照上一次tableSize位运算得到的就是做一次左移1位运算,
假设当前tableSize是16的话 16转为二进制再向左移一位就得到了32 即 16 << 1 == 32 即扩容后的容量,也就是说扩容后的容量是当前
容量的两倍,但记住HashMap的扩容是采用当前容量向左位移一位(newtableSize = tableSize << 1),得到的扩容后容量,而不是当前容量x2