HashMap实现原理
HashMap是Map接口基于哈希表(hash table)的实现。
在HashMap内部,哈希表的实现是通过数组+单链表/红黑树(JDK1.8)来完成(即采用拉链法解决哈希冲突问题)的,示意图如下:





大体的思路:HashMap的内部维护一个数组(Node<K,V>[] table),该数组的每一个位置存放一个bucket(桶或哈希桶),当添加键值对的时候,首先确定要将该键值对放在哪个桶里(table数组的哪个位置),该桶的结构一开始是单链表,但是如果在某一个桶中放入了过多的键值对,那么这个桶的结构就会由单链表转换为红黑树。
这里说的table数组每个位置存放一个bucket的意思是:实际上table数组的每个位置真正存放的就是一个结点Node,只不过该位置处要存放不止一个Node,这些Node就会形成链表或者红黑树,桶则是指链表或红黑树。我觉得不必过于纠结这些概念,只要明白桶就是装东西的,水桶装水,HashMap里的桶装键值对。
这里的桶只是抽象上的概念,简单来讲,HashMap首先维护一个键值对数组,不在这个数组里的键值对通过与数组中某个位置的键值对发生某种关系,形成单链表或者红黑树结构。HashMap的一切操作都在维护着这种关系,以便达到高效的查找和增删效率。
在源码的注释中,经常出现的bin就是指的键值对的封装体:TreeNode<K,V>或者Node<K,V>。 在源码的注释中,经常出现的bucket是指桶(或叫做哈希桶):盛放了一组结点的单链表或红黑树。
下图虚线内的Node就是table数组实际存放的键值对:
下图示意了一个单链表和一颗红黑树的范畴

键值对的载体:Node<K,V>和TreeNode<K,V>
对于HashMap内部,任意一个键值对都是以Node<K,V>的形式封装的。
可以看到,Node中包含了属性Node<K,V> next,也就是下一个结点。HashMap中的单链表结构就是通过这种方式实现的。
TreeNode<K,V>是Node<K,V>的子类(扩展类),实现了红黑树的性质
主要成员变量
put
putVal()的大体思路:先确定该键值对应该存放在哪个桶中,即确定放在table[i]中的i值;如果该桶为空,即table的i位置处还没有存放键值对,则直接将键值对放在table数组中;如果该桶不为空,并且桶结构为红黑树,则将键值对插入到该红黑树中;如果该桶不为空,并且桶结构为链表,则将该键值对插入到链表尾端(插入后,如果链表长度大于8,则将该链表结构转换为红黑树结构)。最后,检查是否需要扩容。
resize
resize()方法有两个作用:初始化table数组;将table数组长度翻倍。
在将table数组长度翻倍的时候,还要重新计算每个键值对应该在的位置,并重新生成链表或者红黑树,这是非常消耗性能的。所以,在使用HashMap的时候,如果能预估出元素的最大容量,最好指定这个容量值。
扩容时机: 当put时,如果发现目前的bucket占用程度已经超过了Load Factor所希望的比例,那么就会发生resize。 大体过程: 先将table数组长度变为原来的2倍,然后将table中的键值对重新分配到各个桶中:循环原table,注意的是,循环次数为数组table的长度,而不是HashMap的size(键值对的个数);在循环体中,对原table中任意一个位置,会判断该位置的结点类型:只有一个、链表、红黑树,然后分类进行重哈希。 如果原table的该位置处的桶结构为链表,则该链表会保留元素顺序:它指的是,假如链表中两个结点a和b,有a.next=b或者a.next.next...=b,那么在重哈希以后,如果a和b仍然在同一链表中,依然会保持a.next=b或者a.next.next...=b的关系;当然只是顺序,next的次数与之前是不同的。
get
看方法getNode:
以上是对HashMap中的几个感觉比较重要的方法的分析,如有不对的地方欢迎指正。
Last updated