Java集合框架(3)——HashMap

来源:互联网 发布:mac怎么设置睡眠时间 编辑:程序博客网 时间:2024/05/21 06:59

动机

我们已经有了数组,ArrayList和LinkedList,为什么有需要HashMap?

因为在之前的数据结构中,最好的搜索方法是有序数组的二分查找和AVL树搜索。它们的最坏情况所搜时间都是O(lgn)。是否有更快的算法?散列表数据结构提供了这样的保证——O(1)的平均搜索时间。

散列与散列函数

散列表是存储数组,散列是通过推演出对象的数值散列码,并将这个散列码映射到散列表中的位置来在散列表中存储对象的过程。

将数映射到散列表中未知的映射关系称为散列函数。

构造哈希表的几种方法
1.直接定址法(取关键字的某个线性函数为哈希地址)
2.除留余数法(取关键值被某个不大于散列表长m的数p除后的所得的余数为散列地址)
3.平方取中法
4.折叠法
5.随机数法
6.数学分析法
常用方法是直接定址法和除留余数法。

冲突与解决

不同的Key值经过哈希函数Hash(Key)处理以后可能产生相同的值哈希地址,我们称这种情况为哈希冲突。任意的散列函数都不能避免产生冲突。

闭链法

若key1,key2,key3产生哈希冲突(key1,key2,key3值不相同,映射的哈希地址同为key),用以下方法确定它们的地址

线性探测法

若当前key与原来key产生相同的哈希地址,则当前key存在该地址之后没有存任何元素的地址中
key1:hash(key)+0
key2:hash(key)+1
key3:hash(key)+2

二次探测法
若当前key与原来key产生相同的哈希地址,则当前key存在该地址后偏移量为(1,2,3…)的二次方地址处
key1:hash(key)+0
key2:hash(key)+1^2
key3:hash(key)+2^2

开链法

相同key值的节点存储在一个链表上。key值不同的链表头节点组成一个数组。

HashMap使用的就是这种方法。

HashMap的死循环问题

从前我们的Java代码因为一些原因使用了HashMap这个东西,但是当时的程序是单线程的,一切都没有问题。后来,我们的程序性能有问题,所以需要变成多线程的,于是,变成多线程后到了线上,发现程序经常占了100%的CPU,查看堆栈,你会发现程序都Hang在了HashMap.get()这个方法上了,重启程序后问题消失。但是过段时间又会来。而且,这个问题在测试环境里可能很难重现。

我们简单的看一下我们自己的代码,我们就知道HashMap被多个线程操作。而Java的文档说HashMap是非线程安全的,应该用ConcurrentHashMap。

HashMap会用一个链表数组来做分散所有的key:

    /**     * The table, initialized on first use, and resized as     * necessary. When allocated, length is always a power of two.     * (We also tolerate length zero in some operations to allow     * bootstrapping mechanics that are currently not needed.)     */    transient Node<K,V>[] table;

当一个key被加入时,会通过Hash算法通过key算出这个数组的下标i,然后就把这个键值对插到table[i]中,如果有两个不同的key被算在了同一个i,那么就叫冲突,又叫碰撞,这样会在table[i]上形成一个链表。

我们知道数组大小是不可变的,当HashMap存放元素增加到一定数目(threshold,可通过负载因子和容量相乘获得,负载因子loadFactor可以在初始化时设置,默认是0.75),为了减少碰撞,会执行重哈希操作reHash(),reHash是一个很笨重的操作,要新建一个更大的链表数组,把原数组中的元素重新计算hash值,填入新数组,然后用新数组替代原数组。

因此,如果并发操作HashMap,会有可能在一个线程执行reHash()的时候get或set,这时有可能会使链表出现循环链表,导致程序出现infinite loop——死循环,这是HashMap的一个缺点,但并不打算改正它,因为有线程安全的ConcurrentHashMap来提供并发下的哈希表实现。

HashMap的细节

1,HashMap实现Map接口;Map接口内部定义了Entry接口,提供Map存储的键K值V对元素的方法。
2,HashMap用数组存储数据,用单链表或红黑树来处理哈希冲突。当数据较少使用单链表,较多转使用红黑树。
3,HashMap内部定义了实现Entry接口的Node类和TreeNode类来实现单链表和红黑树,存储元素的table是一个Node数组。
4,HashMap的重要参数:

capacity 容量size  大小,即实际元素数量loadFactor   负载因子,是个不大于1的正小数,影响HashMap的扩容操作threshold    HashMap扩容的触发阈值,等于capacity*loadFactor

capacity和loadFactor均有默认值,分别是16和0.75,可以调用HashMap的构造函数在初始化是设置。capacity的数值必须是2的次幂。

需要注意capacity,size和table.length的区别,capacity是允许存放元素数量的上限,size是现实存入的元素数量,table.length是存放元素的数组长度。

5,HashMap使用的Hash算法不是hashcode对table.length取模,而是hashcode与length-1做“与”运算。这样同样可以保证分不平均,同时避免了除法运算效率提高。

6,length必须是2的整数次幂,这样length-1一定是奇数,这样便保证了hashcode&(length-1)的最后一位可能为0,也可能为1(这取决于h的值)。假设length-1是偶数的话,最后一位一定是0,那么与运算结果最后一位也一定是0,这样元素就只能存在下标是偶数的位置,只有一半空间被利用,不合理。

7,当数据元素量超过阈值,会触发resize()方法,resize方法会根据情况扩容,capacity,length扩大一倍,改链表为红黑树。这一操作消耗巨大。同时可能引发循环链表问题,这也是HashMap线程不安全的原因所在,也因此产生了ConcurrentHashMap。

8,多线程下HashMap的死循环问题:
因为是链表结构,那么就很容易形成闭合的链路,这样在循环的时候只要有线程对这个HashMap进行get操作就会产生死循环。在单线程情况下,只有一个线程对HashMap的数据结构进行操作,是不可能产生闭合的回路的。在多线程并发的情况下才会出现这种情况,那就是在put操作的时候,如果size>initialCapacity*loadFactor,那么这时候HashMap就会进行resize操作,随之HashMap的结构就会发生翻天覆地的变化。很有可能就是在两个线程在这个时候同时触发了resize操作,产生了闭合的回路。

9,HashMap和Hashtable的区别:

HashMap是非synchronized的,因此单线程HashMap快但线程不安全;HashMap可以接受K,V是null;HashMap是Fail-Fast的,关于Fail-Fast,参考:http://blog.csdn.net/chenssy/article/details/38151189

10,简述HashMap的Put流程:

->元素<K,V>->K.hashcode()->hash()->确定存放的bin->如果bin为空,直接存放;如果非空,比较K值(K.equals)->如果相等,比较V(V.equals())->如果相等,相同元素抛弃;如果不等,替代->插入完成后坚持是否需要resize

11,简述HashMap的Get流程:

->K->K.hashcode()->hash()->确定如果存在应该存放的bin->如果bin为空,返回空->如果非空,遍历bin中元素(链表或红黑树)->遍历过程中使用k.equals()比较K的值,找到K值相同的元素返回->如果找不到返回空。

12,为什么String, Interger这样的wrapper类适合作为键?

String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。

参考:
http://www.cnblogs.com/ITtangtang/p/3948406.html
HashMap源码

0 0
原创粉丝点击