hashmap 中 hash 函数怎么是是实现的?还都有哪些 hash 的实现方式
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了hashmap 中 hash 函数怎么是是实现的?还都有哪些 hash 的实现方式相关的知识,希望对你有一定的参考价值。
参考技术A HashMap是对数据结构中哈希表(HashTable)的实现,Hash表又叫散列表。Hash表是根据关键码Key来访问其对应的值Value的数据结构,它通过一个映射函数把关键码映射到表中一个位置来访问该位置的值,从而加快查找的速度。这个映射函数叫做Hash函数,存放记录的数组叫做Hash表。
在Java中,HashMap的内部实现结合了链表和数组的优势,链接节点的数据结构是Entry
,每个Entry对象的内部又含有指向下一个Entry类型对象的引用,如以下代码所示:
static
class
Entry
implements
Map.Entry
final
K
key;
V
value;
Entry
next;
//Entry类型内部有一个自己类型的引用,指向下一个Entry
final
int
hash;
...
在HashMap的构造函数中可以看到,Entry表被申明为了数组,如以下代码所示:
public
HashMap()
this.loadFactor
=
DEFAULT_LOAD_FACTOR;
threshold
=
(int)(DEFAULT_INITIAL_CAPACITY
*
DEFAULT_LOAD_FACTOR);
table
=
new
Entry[DEFAULT_INITIAL_CAPACITY];
init();
在以上构造函数中,默认的DEFAULT_INITIAL_CAPACITY值为16,DEFAULT_LOAD_FACTOR的值为0.75。
当put一个元素到HashMap中去时,其内部实现如下:
public
V
put(K
key,
V
value)
if
(key
==
null)
return
putForNullKey(value);
int
hash
=
hash(key.hashCode());
int
i
=
indexFor(hash,
table.length);
...
Hashmap的实现原理
问题1)hashmap的实现原理?(Hashmap的数据结构)
HashMap的实现原理是hash函数(hashing/散列),通过put(key,value)和get(key,value)插入值和取出值。
取出值和放入值的过程就叫做散列法(哈希函数)
index == HashCode(key) / (length-1),数组中存储entry对象,entry对象存储key和value(hash/next)
get方法同理。
HashMap的存储结构:初始化时候是长度为16的数组。数组存放了entry类对象,entry类的
HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。
Entry对象的属性:
static
class
Entry
implements
Map.Entry
{
final
Kkey; // key key
不能变。
Vvalue; //
值
Entrynext; // next
指针
final
int
hash; //
计算出来的hash 值 (hashCode值)
...
//More code goes here
}
问题2)当put或get的hashCode相同时候?
Put方法中hashCode值相同
HashMap使用数组和链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。
这种方法是最简单的,也正是HashMap的处理方法。
Get方法中hashCode值相同
根据hashCode找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象
问题3)如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?存储空间不足时候?
默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
问题4)重新调整HashMap大小存在什么问题?
HashMap非线程安全。
当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。
问题5)为什么String, Interger这样的wrapper类适合作为键?
String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。
1. 我们可以使用自定义的对象作为键吗? 这是前一个问题的延伸。当然你可能使用任何对象作为键,只要它遵守了equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会再改变了。如果这个自定义对象时不可变的,那么它已经满足了作为键的条件,因为当它创建之后就已经不能改变了。
问题6)我们可以使用CocurrentHashMap来代替Hashtable吗?
这是另外一个很热门的面试题,因为ConcurrentHashMap越来越多人用了。我们知道Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashMap当然可以代替HashTable,但是HashTable提供更强的线程安全性。看看这篇博客查看Hashtable和ConcurrentHashMap的区别。
问题7)hashmap的初始化长度是多少?为什么这样规定?为什么每次扩展的空间必须是2的幂次?
1,初始化的长度是16
2,为了减少冲突,
问题8)如何实现一个尽量均匀分布的Hash函数呢(减少冲突)?
我们通过利用Key的HashCode值来做某种运算。如何进行位运算呢?
有如下的公式(Length是HashMap的长度):
index= HashCode(Key) & (Length - 1)
下面我们以值为“book”的Key来演示整个过程:
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算法的结果就是均匀的。
问题8)为什么hashmap在高并发情况下会死锁?
问题9)在Java8中对hashmap做了那些优化?
以上是关于hashmap 中 hash 函数怎么是是实现的?还都有哪些 hash 的实现方式的主要内容,如果未能解决你的问题,请参考以下文章