HashMap原理
先以一个简单的例子来理解hashmap的原理。在Java中先随机产生一个大小为20的数组如下:
hash表的大小为7,将上面数组的元素,按mod 7分类如下图:
将这些点插入到hashmap中(简单hashmap)后如下图:
由上图可知:
① hashmap是用链地址法进行处理,多个key 对应于表中的一个索引位置的时候进行链地址处理,hashmap其实就是一个数组+链表的形式。
② 当有多个key的值相同时,hashmap中只保存具有相同key的一个节点,也就是说相同key的节点会进行覆盖。
③在hashmap中查找一个值,需要两次定位,先找到元素在数组的位置的链表上,然后在链表上查找,在HashMap中的第一次定位是由hash值确定的,第二次定位由key和hash值确定。
④节点在找到所在的链后,插入链中是采用的是头插法,也就是新节点都插在链表的头部。
⑤在hashmap中上图左边绿色的数组中也存放元素,新节点都是放在左边的table中的,这个在上图中为了形象的表现链表形式而没有使用。
HashMap
上面只是简单的模拟了hashmap 真实的hashmap的基本思想和上面是一样的不过更加复杂。HashMap中的一个节点是一个Entity 类如下图:
Entry是HashMap的内部类 包含四个值(next,key,value,hash),其中next是一个指向 Entry的指针,key相当于上面节点的值 value对应要保存的值,hash值由key产生,hashmap中要找到某个元素,需要根据hash值来求得对应数组中的位置,然后在由key来在链表中找Entry的位置。HashMap中的一切操作都是以Entry为基础进行的。HashMap的重点在于如何处理Entry。因此HashMap中的操作大部分都是调用Entry中的方法。可以说HashMap类本身只是提供了一个数组,和对Entry类中方法的一些封装。
下面从源码方面对 HashMap进行解析:
①HashMap的继承关系
1 | public class HashMap<K,V> |
从上面可以看到HashMap继承了AbstractMap , 并且实现了Cloneable, Serializable 接口。
②HashMap的构造函数
下面的代码都是经过简化处理的代码,基本流程不变只是为了更好的理解修改和删除了一部分内容
1 | public HashMap(int initialCapacity, float loadFactor) { |
上面的代码要做几点说明:
①填装因子:loadFactor 表示填装因子的大小,简单的介绍一下填装因子:假设数组大小为20,每个放到数组中的元素mod 17,所有元素取模后放的位置是(0–16) 此时填装因子的大小为 17/20 ,装填因子就为0.85啦,你装填因子越小,说明你备用的内存空间越多,装填因子的选定,可以影响冲突的产生,装填因子越小,冲突越小。
②HashMap初始化过程就是新建一个大小为capacity,类型为Entry的数组,Entry上面已经介绍过这个类,包含一个指针一个key,一个value,和一个hash。capacity是2的次幂,至于为什么是2的次幂后面会有介绍的。
下面是另外两个构造函数1
2
3
4
5
6
7
8
9 public HashMap(int initialCapacity) {
HashMap(initialCapacity, 0.75);
//调用了上面的构造函数,只不过使用了默认的填装因子0.75
}
public HashMap() {
HashMap(16, 0.75);
//生成一个table大小为16,填装因子0.75的HashMap
}
③由上可知如果用户直接使用HashMap()构造函数来new一个HashMap 会生成一个大小为16,填装因子为0.75的 HashMap。
③HashMap中的put(key,value)函数
还是先上源码
1 | public V put(K key, V value) { |
对上面代码做几点说明:
① HashMap中的key可以为 null ,此时hash=0,为什么key可以为null,因为HashMap中放的元素是Entry,而Entry包含了4个值(key,value,hash,next),key为 null 时不影响Entry映射到HashMap中。
②hash(key),产生一个正整数,这个整数与key相关。这个hash(key)函数比较关键,后面会进行说明。
③用户插入的(key,value)对不是直接放到HashMap中的,而是用(key,value)以及后面由key value产生的hash,new一个Entry对象后再插入到HashMap中的。
④如果对应的链上有一个hash值个key相同的Entry则覆盖value值,不new Entry对象,如果没有会先new 一个对象在将其插到对应的链上。(其中可能会涉及到扩充HashMap)。
下面看看hash(key)函数
1 | final int hash(Object k) { |
hash函数的作用是使hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。
④HashMap中的get(Object key)函数
上源码
1 | public V get(Object key) { |
上面代码的几点说明
① 通过key来链表中查找元素包括两个过程,先由hash找到链(hash由key产生,不同的key可能产生相同的hash值,相同的hash值放在同一条链上),再用key在链上找。
② 如果key为null则只在table[0]和其链上查找,因为key为null都放在table[0]及其链上了。
③因为在HashMap中查找到的是Entry对象,返回的值是Entry对象的value值。
重点Entry类
其实理解HashMap最重要的在于理解Entry类,Entry类相当于链表中的一个节点,是HashMap操作的基础。下面主要从Entry类的几个方法来理解Entry类和HashMap的关系。
①Entry中的addEntry( hash, key, value, bucketIndex)函数
在HashMap中调用put(key,value)时,如果(key,value)是首次加入到HashMap中,就会调用
addEntry( hash, key, value, bucketIndex)函数,将其加入到table表对应的位置中(注意是table中,不是后面的链中,首次加入的元素都是采用的头插法)。下面是源码:
1 | void addEntry(int hash, K key, V value, int bucketIndex) { |
上面代码的几点说明:
①bucketIndex是由hash取模后对应于table表中的哪个位置。indexFor(hash, table.length)其实是一个取模函数。它的实现很简单 hash& (length-1),就是用hash值与上table表的长度减1。
②并不是对每一个(key,value)对都产生一个Entry对象,只是(key,value)对首次放到HashMap中时,或者HashMap中没有相同的key时,才产生一个Entry对象,否则如果有相同的key则会直接将value值赋个Entry的value。
③新产生的Entry都是放在了table中,也就是链表的首位,采用链表的头插法。
②HashMap中的keySet()函数
作用:返回HashMap中key的集合。keySet是HashMap中的内部类:
1 | public Set<K> keySet() { |
keySet是用来遍历整个HashMap的,因此是十分重要的,下面做几点说明。
①keyset中有一个迭代器可以迭代的获取下一个key的值,通过key的值就可以获得Entry对象了。
②对应key的迭代遍历是table表中由左向右,由上向下进行的,也就是先遍历table[0]这条链上的,然后遍历table[1]这条链上的依次往下进行。
③newKeyIterator具体实现这里就不多介绍,只要知道上面的功能怎么实现就可以了。
下面是利用keyset来实现遍历HashMap的例子:
1 | HashMap<Integer, Integer> hashMap=new HashMap<Integer, Integer>(); |
②HashMap中的entrySet()函数
作用:返回HashMap中Entry的集合。对于entrySet这里就不上源码了,举一个使用entrySet遍历HashMap的例子:
1 | HashMap<Integer, Integer> hashMap=new HashMap<Integer, Integer>(); |
使用entrySet()函数遍历比keySet()函数遍历快,因为keySet()函数是先通过entrySet()求出key然后在通过key来遍历获得Entry的,所以速度比entrySet()慢很多。
参考文献:
①http://blog.csdn.net/jiary5201314/article/details/51439982
②http://www.cnblogs.com/chenssy/p/3521565.html