1.HashMap的存储方式是数组加链表,主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对;当不同的key经过hash计算得出的index值相同时,就需要在数组里添加一个链表来存储index相同的元素,HashMap的整体结构如下:
简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。
2.需要注意的是,新来的Entry节点插入链表时,使用的是“头插法”。之所以把新来的节点放在头节点,是因为HashMap的发明者认为,后插入的Entry被查找的可能性更大,这就是HashMap的底层原理,
HashMap的默认初始长度是16,并且每次自动扩展或是手动初始化时,长度必须是2的幂
初始长度16是为了服务于从Key映射到index的Hash算法
从Key映射到HashMap数组的对应位置,会用到一个Hash函数,比如调用 hashMap.put("book", 0) ,插入一个Key为“book"的元素。这时候我们需要利用一个哈希函数来确定Entry的插入位置(index):
index = Hash(“book”)---->> index = HashCode(Key) & (Length - 1)//这里Key为book,Length是HashMap的长度
整个过程如下:
1)计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。
2)假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。
3)把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。可以说,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。
假设HashMap的长度是10,重复刚才的运算步骤
单独看这个结果,表面上并没有问题。我们再来尝试一个新的HashCode 101110001110101110 1011 :
再换一个HashCode 101110001110101110 1111 试试 :
虽然HashCode的倒数第二第三位从0变成了1,但是运算的结果都是1001。也就是说,当HashMap长度为10的时候,有些index结果的出现几率会更大,而有些index结果永远不会出现(比如0111)!
这样,显然不符合Hash算法均匀分布的原则。
反观长度16或者其他2的幂,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。
(这点也涉及到了JAVA中hashCode()与equals()区别与作用:hashCode也可以用来比较对象是否相同,且效率高于equals,equals的效率很低下。但hashCode()并不是完全可靠,不同的对象也可能生成相同的hashcode,而equals是完全可靠的。
所有对于需要大量并且快速的对比的话如果都用equal()去做显然效率太低,所以解决方式是,每当需要对比的时候,首先用hashCode()去对比,如果hashCode()不一样,则表示这两个对象肯定不相等(也就是不必再用equal()去再对比了),如果hashCode()相同,此时再对比他们的equal(),如果equal()也相同,则表示这两个对象是真的相同了,这样既能大大提高了效率也保证了对比的绝对正确性!)
(hashCode值的计算方式:具体可以参考不同版本的jdk api内的 hashCode函数)
3.HashMap非线程安全
1)Hashmap在插入元素过多的时候需要进行Resize,Resize的条件是
HashMap.Size >= Capacity * LoadFactor。
2)Hashmap的Resize包含扩容和ReHash两个步骤,ReHash在并发的情况下可能会形成链表环。
https://blog.csdn.net/wufaliang003/article/details/80219296