Java的HashMap
来源:互联网 发布:比淘宝好的购物网站 编辑:程序博客网 时间:2024/05/29 04:51
前段时间,阿里巴巴在云栖大会上发布了阿里巴巴Java开发规约插件,在第一时间使用这款插件感觉十分强大,对于Java开发人员来说是一款非常好的礼物,感谢阿里。
在使用插件检测公司之前开发的代码时,发现了许多的“集合初始化时,指定集合初始化大小。”的问题,在公司同事之间进行了一些讨论,同时在网上查询了关于该问题的资料,发现自己对于HashMap此类集合了解的十分浅显,所以查询了许多资料来深入学习HashMap。
定义
众说周知,HashMap是一个用于存储Key-Value键值对的集合。每一个键值对也叫做Entry,这些键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。其中,每一个HashMap的值都为空。 默认初始容量 (16) 和默认加载因子 (0.75),HashMap的最大容量值为2^30,扩容倍数为2。
也就是说,当我们new一个HashMap的时候它的长度为16,当存储到第(16*0.75=12)个的时候,HashMap会进行扩容,扩容到32,依次类推,所以HashMap的大小都是2^n。
方法
HashMap最常用的方法是put和get方法。
put方法
调用put方法时会利用一个哈希函数来确定Entry的插入位置(index):index = Hash(“apple”)。
所谓Hash函数按照定义可以实现一个伪随机数生成器(PRNG),从这个角度可以得到一个公认的结论:哈希函数之间性能的比较可以通过比较其在伪随机生成方面的比较来衡量。对任意一类的数据存在一个理论上完美的哈希函数。这个完美的哈希函数定义是没有发生任何碰撞,这意味着没有出现重复的散列值。在现实中它很难找到一个完美的哈希散列函数,而且这种完美函数的趋近变种在实际应用中的作用是相当有限的。在实践中人们普遍认识到,一个完美哈希函数的哈希函数,就是在一个特定的数据集上产生的的碰撞最少哈希的函数。
HashMap的Hash函数
利用Key的HashCode值来做位运算。Hash(Key) = HashCode(Key) & (Length - 1)
public class Test { public static void main(String[] args) throws Exception { System.out.println("apple".hashCode() & (16-1)); System.out.println("banana".hashCode() & (16-1)); System.out.println("book".hashCode() & (16-1)); }}
输出结果为:10 5 9
1.计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。
2.假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。
3.把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。
此处有一个问题,为什么HashMap的长度是2的幂?
假设初始长度为10,重复上面的位运算结果的二进制为1001
尝试101110001110101110 1011 和 101110001110101110 1111 结果也为1001,也就是说,当HashMap长度为10的时候,有些index结果的出现几率会更大,而有些index结果永远不会出现(比如0111)!显然不符合Hash算法均匀分布的原则。反观长度2的幂,Length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashCode后几位的值。只要输入的HashCode本身分布均匀,Hash算法的结果就是均匀的。
得到index值之后,也就得到了entry值插入的位置。但是由于HashMap的长度是固定的,但是在严格的Hash函数也会出现重复的情况,这样就会得到重复的index值。利用链表来解决这个问题,HashMap数组的每一个元素不止是一个Entry对象,也是一个链表的头节点。每一个Entry对象通过Next指针指向它的下一个Entry节点。当新来的Entry映射到冲突的数组位置时,只需要使用“头插法”插入到对应的链表即可。因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。
get方法
输入的Key做一次Hash映射,得到对应的index:index = Hash(“apple”)
由于刚才所说的Hash冲突,同一个位置有可能匹配到多个Entry,这时候就需要顺着对应链表的头节点,一个一个向下来查找。假设我们要查找的Key是“apple”:
第一步,我们查看的是头节点Entry6,Entry6的Key是banana,显然不是我们要找的结果。
第二步,我们查看的是Next节点Entry1,Entry1的Key是apple,正是我们要找的结果。