您的位置:首页 > 编程语言 > Java开发

Java集合学习:HashMap的实现原理和工作原理

2014-11-28 20:06 134 查看
1.概述

基于哈希表的Map接口的实现,提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。此实现假定哈希函数将元素适当的分布在各桶之间,可为基本操作(get和put)提供稳定的性能。迭代性能与HashMap的容量(桶的数量)及其大小成比例。

2.与HashTable的对比

两者最主要的区别在于HashTable是线程安全,而HashMap非线程安全。因此相对而言HashMap性能会高一点,在多线程的环境下若使用HashMap,需要使用Collections.synchronizedMap()方法来获得一个线程安全的集合。//TODOCollections.synchronizedMap()的实现原理
HashMap可以使用null作为key,但建议避免这样使用。当HashMap以null作为key的时候,总是存储到table数组的0号位置(或者说0号位置对应的链表)。

Hash方式不同。HashTable是通过key的hashcode对table数组的长度直接取模,而HashMap对其进行了二次hash,以获取更好的散列。

3.数据结构

在数据结构中有数组和链表来对数据进行存储,但两者却各有优缺点。这里不做详细介绍。

哈希表:综合考虑这两种结构的优点,就是寻址容易,插入删除也容易的数据结构。

哈希表有多种不同的实现方法,拉链法是最常用的一种,即:链表的数组,每个元素存放链表头结点的数组



哈希表是由数组+链表组成的。在数组中,每个元素存储的不是实际的数据,而是一个链表的头结点。关于存储规则,我们这里先简单假设性的讲一下,更具体的实现会在后面章节说明。首先,我们可以得到key的hashcode,然后对其hash(key的hashcode)%len得到,通俗的说就是对key的hashcode进行hash算法,然后对数组长度取模。比如说,一个key的hashcode为108,然后len是16,所以通过计算可以得到12,也就是说108这个数据将存到数组12这个位置的链表中。

现在还有一个问题,就是这个线性的数组+链表是如何实现按键值对来存取数据呐?

在HashMap中,实现了一个静态内部类Entry,其重要的属性有key,value,next。通过属性我们可以看出,Entry就相当于HashMap对键值对实现的bean。HashMap的基础是一个线性数组,而这个数组就是Entry的数组。

transientEntry[]table;
staticclassEntry<K,V>implementsMap.Entry<K,V>{
finalKkey;
Vvalue;
Entry<K,V>next;
finalinthash;


4.存取实现

(1)put()

首先,看一下源码:

publicVput(Kkey,Vvalue){
if(key==null)
returnputForNullKey(value);
inthash=hash(key.hashCode());
inti=indexFor(hash,table.length);
for(Entry<K,V>e=table[i];e!=null;e=e.next){
Objectk;
if(e.hash==hash&&((k=e.key)==key||key.equals(k))){
VoldValue=e.value;
e.value=value;
e.recordAccess(this);
returnoldValue;
}
}
modCount++;
addEntry(hash,key,value,i);
returnnull;
}


然后我们一步一步来看一下上面的代码。

首先对key做null检查。如果key==null,调用putForNullKey方法,将其存储到table[0],因为null的hash值总是0。

key的hashcode()方法会被调用,然后通过hash方法计算hash值,返回int的变量hash。hash值用来找到存储Entry对象的数组的索引。

indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引。

接下来是比较重要的环节了。首先我先举一个例子,然后根据例子来进行分析。假如我们现在有3个键值对(4,"xiaohei"),(6,"xiaobai"),(4,"xiaohong"),然后key的hashcode方法也很简单,我们就假设4和6都返回一样的值,依次put。首先,put进去第一个键值对,当put第二个键值对的时候,会发现hash方法得到的值是一样的,这个时候就会比较value的值是否相等,因为"xiaohei"不等于"xiaobai",所以会对链表进行迭代,一直到Entry->next是null的时候,将此时这个键值对放到下一个节点;当put第三个键值对的时候,同样发现hash得到的值一样,继而比较value也一样,这个时候就会体会value的值。
通过上边这个例子分析,我们可以看到如果两个key有相同的hash值(也叫冲突),他们会以链表的形式来存储。所以,这里我们就迭代链表。

如果在刚才计算出来的索引位置没有元素,直接把Entry对象放在那个索引上。

如果索引上有元素,然后会进行迭代,一直到Entry->next是null。当前的Entry对象变成链表的下一个节点。

如果我们再次放入同样的key,在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)),如果这个方法返回true,它就会用当前Entry的value来替换之前的value。

(2)get()

还是先看一下源码:

publicVget(Objectkey){
if(key==null)
returngetForNullKey();
inthash=hash(key.hashCode());
for(Entry<K,V>e=table[indexFor(hash,table.length)];
e!=null;
e=e.next){
Objectk;
if(e.hash==hash&&((k=e.key)==key||key.equals(k)))
returne.value;
}
returnnull;
}


当我们理解了HashMap的put方法的工作原理之后,理解get的工作原理就非常简单了。
当你通过一个key准备从HashMap中获取值的时候:

首先对key进行检查。如果key是null,table[0]这个位置的元素将被返回。
接下俩,key的hashcode()方法被调用,同样调用hash方法来计算hash值。
再通过indexFor方法用来计算要获取的Entry对象在table数组中的精确的位置。
在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。

5.重要参数

HashMap有两个重要的参数:初始容量和加载因子。我们这部分将介绍与这方面相关的知识。

从上面的源代码中可以看出:当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,再根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。所以我们当然希望HashMap中的元素分布的尽量均匀一些,尽量使得每个位置上的元素只有一个,那么我们就能节省遍历链表的时间,能够优化效率。

对于任意给定的对象,只要它的hashCode()
返回值相同,那么程序调用hash方法所计算得到的值是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的。

在HashMap中是这样做的:通过indexFor方法来计算该存到哪个索引。其源码如下:

staticintindexFor(inth,intlength){
returnh&(length-1);
}


通过h&(table.length
-1)来得到该对象的索引位置,而当length是2的n次方时,h&(length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。也就是说两者等价不等效。

HashMap的初始容量为16,能够尽量保证分布均匀。另外一个参数,加载因子,这个参数决定了HashMap在何时进行扩容,毕竟初始容量只有16,当数据很多的时候...其默认的加载因子为0.75,也就是说当哈希表的容量超过16*0.75的时候,会进行数组大小的扩展,即rehash过程。这个过程非常消耗性能。将创建一张新表,将原表的数据映射到新表中。所以如果我们能够预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

6.总结

HashMap有一个叫做Entry的内部类,用来存储key-value键值对。

Entry对象是存储在一个叫做table的Entry数组中。

table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。

key的hashcode()方法用来找到Entry对象所在的桶。

如果两个key有相同的hash值,他们会被放在table数组的同一个桶里面。

key的equals()方法用来确保key的唯一性。

value对象的hashcode()方法根本一点用也没有。

参考文档:HashMap工作原理
深入Java集合学习系列:HashMap的实现原理
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: