1. HashMap的主要用途是什么?
思考: HashMap相比于其他Java集合有什么联系,HashMap实现了Map接口,而Map接口是一种增删改查键值对的规则,那他有什么特点呢,多线程相关,键值对的性质,HashMap没有对多线程做加锁,相比于数组,栈,队列,树,链表,HashMap是无序结构,也就说内部没有一个有结构的顺序来维护,同时因为Map接口以Key-Value来存取数据,导致其允许有且一个null值。
HashMap的特点决定了他的用途 :因为其内部是无序的,因此不能作为排序的容器,Map接口同时记录了键和值,可以存储配置参数等,同时增删改查的时间复杂度为o(1)。(HashSet内部实现也有HashMap的参与)
2. HashMap的一些机制?即如何实现的Map接口的
思考:Map接口包括
put(Key,Value),
get(Key),
remove(Key),
containsKey(Key),
containsValue(Value),
entrySet(),
size()
其中最主要的是put,get方法
put(Key Value)的具体实现方法
V putVal(hashcode,key,value)
/* 参数主要包括 hashcode,key,value,返回值 V 和value同一类型
* 在声明HashMap,其构造方法 初始化了threshold 阈值,也就是HashMap的容量的3/4,默认为16 * 3/4 = 12
* 但是此处运用了懒加载的思想,即在第一次putVal时才会实际初始化Node数组
*/
V get(Key) 具体实现为
Node getNode(hash,Key)
/* 相当于 putVal 的逆操作,看 putVal 具体实现即可
* 同时 remove(Key) 的过程也是 putVal的 逆过程
*/
V remove() 具体实现为
Node removeNode(hash,Key,Value)
/* removeNode 逻辑是找到目标Node,
* 如果node的引用在Node数组中,就赋值 Node数组[index]= node.next
* 如果在Node链表中,就通过记录pre结点,删除链表中的node
* 在这个过程,并没有扩容的逆过程机制,即扩容是不可逆的,HashMap只能越用越变越大
putVal方法的流程总结如下:(毕竟说的时候不能背源码)
- 首先判断Node数组,是否为空,为空需要初始化Node数组,实际调用的是扩容的逻辑,毕竟初始化也算扩容
- 计算Key的hashcode与数组长度的位"与"运算,结果为当前Key映射的Node数组下标index
- 将hash,Key,Value,组合成Node对象,Node对象容器内持有hash,Key,Value,next;如果Node数组[index]为null直接存入Node对象
- 如果不为null,判断Node容器内hash是否与存入hash相同,相同则将value替换容器内旧value
- 如果hash不同,则进入hashcode处理碰撞的过程,以此Node为链表头结点,递归判断Node的hashcode,如果相同,将value替换容器内旧value
- 如果Node节点next为空,即Node链表中,没有hashcode相同的情况,则在链表尾部插入新的组合成的Node对象(尾插法)
- 最后,只要存入的Key-Value生成了新Node,而非替换旧有Node中Value,HashMap的size++,当size大于阈值时,就会触发扩容机制
- 如果只是替换,旧有Node的Value将作为返回值
hashcode碰撞过程:理想状态下HashMap内存取Key-Value,唯一依赖参数就是Key的hashcode,如果两个不同Key的hashcode,映射Node数组index相同,就相当于存两个不同的Key-Value到一个Node[index]下,这就产生了碰撞
那么如何解决呢?
HashMap此时引入了链表,即之前提到的Node对象容器中next引用,用next来应对两个不同Node hashcode碰撞的问题,将key-value组成Node对象,并由前Node.next持有此对象的引用,形成链表
链表转换为红黑树的过程
在进行步骤5时,遍历链表时,插入新Node节点后,链表长度binCount大于8时执行treeBin,将链表转换为红黑树,红黑树的节点TreeNode内部成员如下
TreeNode parent
TreeNode left
TreeNode right
TreeNode prev
boolean red
* Entry<K,V> before,after
/* TreeNode继承自LinkedHashMap.Entry,LinkedHashMap.Entry持有前后节点引用
* TreeNode持有 父节点,左右子节点,qian
*/
- 首先将HashMap.Node节点转换成TreeNode,然后将Node链表转换成具有前驱后继的双链表
- ....(之后在分析) (平衡二叉树的旋转不影响二叉树的中序遍历)
-
HashMap中的设计模式
在HashMap类中出现很多实现接口的内部类如KeyIterator,ValueIterator,EntryIterator
,KeySet,ValueSet,EntrySet,Values,内部类有很多好处,可以动态设置类的部分接口功能,(像View.OnClickListener),内部类也可以实现多重继承,同时符合开闭原则和单一职责原则,这样可以避免一个类层级上实现多个概念上没有关系接口 “java内部类有什么用-zhihu”
内部类之间可以相互调用,他们的具体实现又依赖外部实现的具体方法+自身继承的抽象,内部类相当于依靠外部类方法的抽象模块,通过模块之间相互调用,实现更高级抽象的操作,但这种抽象包含在外部类内,通过外部类的对外暴露接口调用,简直太妙了!
HashMap的扩容机制和 位运算 &
HashMap在当插入的Node结点总数超过初始设定的阈值时,就会触发扩容机制,比如将Node数组的长度由16扩展为32,同时在初始化调用putVal时也会触发扩容机制
Node[] resize() 中实现扩容的逻辑
/* 先遍历旧Node数组中的元素,判断是Node单结点还是链表
* 如果是单节点,则重新运算结点hash 按位与 新Node数组长度-1
* 如果是链表,则将按照链表中Node结点hash 按位与 旧Node数组长度
* 来区分在 “高进位”中是否有有效标记
* 并将其分为 ”高进位有效“的链表和”高进位无效“的链表
* ”高进位无效“的链表插入到新Node数组的原始index下标下
* “高进位有效”的链表插入到xinNode数组“原始index+旧Node数组长度”下标下
在resize过程中,依赖位运算 & 来平衡数组各下标内的链表长度
位运算 & : 有些像二进制运算中的”十进制的取模运算“,将长二进制数,映射到不大于某短二进制数的目标二进制数,按位与计算的是低位1111的标记,从而映射到短二进制数size=16的数组下标,当触发数组扩容时,同时变化的还有短二进制数的位数例如由1111 -> 11111,再重新将旧数组内链表node的hash 按位与 新增的高进位 1(第5位),如果在高进位中存在有效标记,那么就把此node存入到代表此高进位的新数组下标 (index+16)
3. 其他问题:
- HashMap的数组长度为什么是2的倍数? 因为HashMap的hash查找和扩容机制依赖位运算,所以必须长度是2进制数
- HashMap的Key对象有什么局限? Key对象最好是final类型,即不可更改的对象,同时其hashcode也是定值,这样不会在Key变化后引起Key.hashcode的变化
- HashMap如何应对多线程使用中的并发更改? 在所有Java Collection中,迭代器都具有fail-fast机制,HashMap的PutVal,Remove等导致Node节点总数变化的操作都会触发 modCount++,当迭代器在遍历时,会比较modCount是否一致,如果不一致代表HashMap内部变化了