一、hashmap数据结构
图1.是hashmap的数据结构。它有两部分构成:横向的数组和纵向的链表。在向hashmap放入数据的时候,hashmap根据key的hashcode计算对应hash值,并根据hash值计算在数组中的位置。如果产生碰撞(其他key也存放在该位置),那么把新加入的值放入链表的队头。删除数据的时候,也是首先计算key的hash值,然后,找到对应的数组位置,遍历链表,根据key找到对应元素,然后删除。具体的添加和删除逻辑请参见后续章节。
图1.hashmap数据结构
二、添加元素
图2.是添加元素代码。如果key为null,那么执行putForNullKey()方法。该方法负责把key为null的元素放置到下标为0的数组单元所对应的链表中。如果key不为null,那么根据key的hashcode计算hash值,并根据hash值计算需要放置的数组下标。图3.是计算下标的过程。根据下标找到数组单元(存放链表地址),然后遍历链表。如果在链表中发现与key相等的元素,那么直接用新的value覆盖老的value,并返回老的value。如果没有找到对应的元素,那么直接插入新的元素。
图2.put元素
图3.计算数组下标
图4.就是添加新元素的代码。新建一个链表节点,把该节点作为头节点,把原有链表放入该节点的尾指针。并把key的hash值,key,以及value放入该节点。另外,要判断该hashmap是否需要扩容。如果size大于或者等于threshold,那么重新分配hashmap容量,新容量为原来容量的2倍。下节详细介绍hashmap的扩容机制。
图4.添加新的元素
三、hashmap扩容
图5.是hashmap的扩容代码。首先判断原来的数组长度是否等于MAXIMUM_CAPACITY(最大容量,缺省值为1<<31)。如果等于该值,那么说明已经达到容量上限。这时,把threshold设置为最大整数,这样做的目的是不允许再被扩容(图4中扩容的先决条件是size大于threshold,现在threshold取值最大整数,因此,size不可能再大于threshold,所以,也就会不会再执行扩容逻辑)。如果数组长度没有达到最大容量,那么实例化一个新的数组,容量是现有数组长度的两倍。然后,重新计算所有链表中各个元素在新数组上的“坐标”。并把threshold设置为装填因子与新容量的乘积,比如1000*0.75。
图5.hashmap扩容逻辑
图6.就是重新计算各个元素“坐标”的逻辑。遍历每一个链表,根据链表中每个元素的hash值计算需要放置该元素的新数组下标。找到对应的新数组下标后,在该下标对应的新数组单元中放入指向该元素的指针,并把新数组单元原来的内容放入该元素的next指针(其实就是增加链表头节点逻辑)。
图6.重新计算各个元素”坐标”
四、删除元素
图7和图8是删除元素的代码。根据key的hashcode计算hash值。如果key为null,那么hash值为0。根据hash值计算数组下标。找到下标对应的数组单元,遍历该数组单元对应的链表。找到与key匹配的元素,从链表中删除该元素。如果删除成功,返回删除元素的引用,否则,返回null。
图7. 删除元素
图8.具体删除逻辑
五、hashmap初始化
最后,我们聊下hashmap初始化。Hashmap缺省的初始化容量为16。可以在实例化时,指定初始化容量和装填因子。注意:hashmap实例化的时候,并不一定按照你指定的容量进行初始化的。如果你指定的值是2的幂,那么按照指定值进行初始化。否则,取大于指定初始化值的2的幂的最小值为新的初始化值。
这样做的目的是为了减少碰撞概率。我们看下计算数组下标的方法逻辑(如图3所示):h&(length-1)。由于初始化长度和扩容后长度都是2的幂,因此,长度减1后,所得整数的二进制位数都为1。比如:初始化值取8,那么8-1=7。7的二进制是111。一个整数与7相与得到的结果比与6相与得到的结果重复率要小。