Map数据结构相比较List而言无疑在底层实现变的更复杂了一些,Map系列的源码学习尝试从最基本最常用的HashMap成员开始。其中设计思路部分取自于JDK1.8HashMap源码中英文的翻译,这部分内容感觉对HashMap的源码整体实现的学习和理解还是有一定帮助,值得一看。
HashMap 设计思路
初试容量和负载因子
HashMap基于哈希表的Map接口实现,并允许null键和null值。 (HashMap类大致相当于Hashtable,除了它是不同步的并允许null)。这个类不保证映射的顺序。这个实现提供了基本操作(get和put)的恒定时间性能,假设散列函数在桶中正确地分散条目。 对集合视图的迭代时间与HashMap实例的容量和键值映射的数量相关。 因此,如果要求迭代性能较好,不要将初始容量设置得太高(或负载系数太低),这一点非常重要。
HashMap的一个实例有两个影响其性能的参数:初始容量和负载因子。容量是哈希表中的桶数,初始容量只是创建哈希表时的容量。负载因子是已存储条目数目与容量的一个比例,超过这个比例会触发哈希表的自动扩容。 当散列表中的条目数量超过负载因子和当前容量的乘积时,散列表被重新散列(即,重建内部数据结构),使得散列表具有大约两倍的桶数量。
一般来说,默认负载因子(0.75)在时间和空间成本之间提供了良好的平衡。 较高的值会减少空间开销,但会增加查找成本,因为碰撞会跟着增加,就会逐渐失去哈希表的优势。在设置其初始容量时,应考虑映射中的预期条目数及其负载系数,以便减少扩容的次数。 如果初始容量乘以负载系数大于需要存储的最大条目数,则不会发生重新擦除(扩容)操作。
如果许多映射要存储在HashMap实例中,则使用足够大的容量来创建映射将比不断地扩容来满足映射条目的存储更加高效。注意,使用具有相同hashCode()的许多键会减慢散列表的性能。为了改善影响,当键是可比较(Comparable)时,此类可以使用键之间的比较顺序来帮助打破关系。
线程不安全
请注意,HashMap此实现未同步。 如果多个线程并发访问散列映射,并且至少一个线程在结构上修改映射,那么它必须在外部同步。(这种结构修改是指添加或删除一个或多个映射的任何操作。不过,仅仅改变该实例已经包含的键相关联的值不是结构修改。)这通常通过在自然地封装映射的某个对象上同步来实现 。
如果没有这样的对象存在,映射应该使用Collections.synchronizedMap方法“包装”。这最好在创建时完成,以防止意外的不同步访问,如:
所有这个类的“集合视图方法”返回的迭代器都是失败的:如果在创建迭代器之后的任何时候结构地修改映射,除了通过迭代器自己的remove方法之外,迭代器将抛出一个ConcurrentModificationException。因此,面对并发修改,迭代器快速而干净地失败(fail-fast),而不是在将来的未确定时间冒任意的,非确定性的行为。
注意,迭代器的故障快速行为不能被保证,因为一般来说,在不同步并发修改的情况下不可能做出任何硬的保证。(意思就是有时候会跑出错误有时候不一定会抛出。)故障快速迭代器在尽力而为的基础上抛出ConcurrentModificationException。 因此,编写一个依赖于此异常的程序是正确的是错误的:迭代器的fail-fast行为应该仅用于检测bug。
HashMap 源码分析
构造方法
|
|
在执行下列代码new一个HashMap的时候,只做了一件事,就是将负载因子设置为默认的负载因子值。用默认的初试容量构造一个空的(empty)HashMap。而真正创建HashMap则是在我们第一次添加条目的时候进行。
put(K key, V value)
|
|
treeifyBin
对于数组中链表长度较长的链表(长度大于默认值8)进行二叉树化,但是需要注意的是,先要判断这个数组的长度,如果数组的长度tab.length<64,这个时候没必要去转化为红黑树,而是进行`resize()`扩容。
64,这个时候没必要去转化为红黑树,而是进行`resize()`扩容。>
resize
有两种情况会调用resize()
方法,其一是当第一次在HashMap中添加数据时候,其二是当冲突链表的长度超过TREEIFY_THRESHOLD=8
的时候,会检查当前数组的长度,如果数组长度小于MIN_TREEIFY_CAPACITY=64
也会调用resize()
方法。
|
|
以上可以看出HashMap的数据结构主要由数组、链表和红黑树构成,而put方法的主要流程梳理下来应该是比较清晰的了,这里面有几个关键的点需要注意:
- 如果没发生碰撞,直接new一个新node,赋值到tab数组中。
- 如果发生碰撞,则根据碰撞数目的大小,用两种方式来处理碰撞:
- 链表结构(桶内条目没超过转变阀值)
- 红黑树结构(桶内条目超过转变阀值)
- 遇到key值相同的,用新值替换旧值
- 条目数目超过负载阀值(数组长度*负载因子0.75),进行自动扩容,数组长度翻倍
另外,在putVal方法中还涉及到resize(…)、newNode(…)、treeifyBin(…)等具体功能方法,以及Node
get(Object key)
|
|
知道put的流程之后,get查询的源码阅读起来便变得毫无压力,说一下,JDK源码中的判断逻辑确实很严密,这是应该学习的地方。
remove(Object key)
|
|
-EOF-