HashMap源码分析

来源:互联网 发布:excel2013破解软件 编辑:程序博客网 时间:2024/06/06 10:57

1、 HashMap概述:
HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
大家都知道hashmap基于数组+链表的形式实现的,并且允许key,value都是null。接下来我们就来看看,hashmap的实现,为什么可以都为null.

类结构
这里写图片描述
这个是HashMap的类的结构关系。继承了AbstractMap,实现了这么几个接口。Map接口实现为了表明层次结构。AbstractMap抽象类已经实现了Map接口的大量的方法,那么HashMap就可以实现自己的主要功能。

看看类里面的参数
这里写图片描述
这里写图片描述
接下来我们一个一个的分析。

初始化的默认的大小16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

最大的容量
static final int MAXIMUM_CAPACITY = 1 << 30;

加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

创建一个空的Entry
static final Entry<?,?>[] EMPTY_TABLE = {};

座位调整的表,源码解释。必须是2的倍数,等会分析这个。一个很好的机制。尖括号里面有k,v但是,要被这个给限制,不能显示,大家知道就好。
transient Entry <> [] table = (Entry<>[]) EMPTY_TABLE;

元素的个数
transient int size;

阈值=加载因子*默认的容量,超过这个大小的时候,扩容
int threshold;

哈希表的加载因子
final float loadFactor;

这个也很重要,用来处理快速失败的时候用。
transient int modCount;

这个我没有怎么看懂,有明白的人,可以留言,一起学习。
static final int ALTERNATIVE_HASHING_THRESHOLD_DEFAULT = Integer.MAX_VALUE;
接下来我们从hashMap的构造方法入手,很关键
这里写图片描述
首先很简单的参数的合法性的校验。
第二个if是为了防止有人恶意创建一个非常大的初始容量。如果太大就默认为MAXIMUM_CAPACITY 大小为 1<<30;
然后调用init()方法。
这里写图片描述
这两个构造方法,其实也是在调用第一个构造方法。
我发现还有一个构造方法。
这里写图片描述
里面传进去的事一个map,具有图中红色圈出来的问题。
感觉道理还是一样的。
这里我们可以看到用map的大小除以了加载因子然后+1和初始容量做比较,然后用max函数取出两个中最大的数,然后在调用第一个构造方法。个人认为是为了保证hashmap的容量,如果直接用map.size()做比较的话,可能出现直接用把空间用满了,后面就要直接扩容了。
可以看到里面还有两个方法
inflatetable(threshould);putAllForCreate(m);
这两个方法我看了一下,不太重要,就一个保证capacity一定是2的次幂。后面讲为什么要一定是2的次幂。
public V put(K key, V value)
这里写图片描述
首先我们看到,当key为空的时候
if (key == null)
return putForNullKey(value);
我们进入这个方法putForNullKey();
这里写图片描述
在这里我们可以看到,null总是在第一位的,然后循环这个Entry,如果里面有已经存在null,就直接覆盖了,没有然后在加入。
然后在来看put中两个重要的方法
int hash = hash(key);
int i = indexFor(hash, table.length);
这里写图片描述
hash是通过key的hashCode()函数,然后经过运算得到的结果。
然后通过indexFor()函数,算出下标的位置。这里有一个知识点给大家讲解一下。
当length总是 2 的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。
这看上去很简单,其实比较有玄机的,我们举个例子来说明:
假设数组长度分别为15和16,优化后的hash码分别为4和5,那么&运算后的结果如下:

h & (table.length-1) hash table.length-1

4 & (15-1): 0100 & 1110 = 0100

5 & (15-1): 0101 & 1110 = 0100

4 & (16-1): 0100 & 1111 = 0100

5 & (16-1): 0101 & 1111 = 0101

从上面的例子中可以看出:当它们和15-1(1110)“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,4和5会被放到数组中的同一个位置上形成链表,那么查询的时候就需要遍历这个链 表,得到4或者5,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hash值会与15-1(1110)进行“与”,那么 最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!而当数组长度为16时,即为2的n次方时,2n-1得到的二进制数的每个位上的值都为1,这使得在低位上&时,得到的和原hash的低位相同,加之hash(int h)方法对key的hashCode的进一步优化,加入了高位计算,就使得只有相同的hash值的两个值才会被放到数组中的同一个位置上形成链表。
所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
然后我们就要往里面放元素。
我们可以看到有这么个比较
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
这个的意思就是说,如果插入B,但是已经存在一个A和B的地方重复了,这个时候我们就要判断是覆盖,还是以链式结构添加。我们就要判断key是否一样。如果一样,就用我们一直强调的key不能重复,就是覆盖,不相等在添加。
我们就要往Entry里面添加元素了。所以我们要俩看看这个类。
这里写图片描述
很简单的几个变量定义。next用来处理hash冲突的。
接下来我们看看怎么往里面加
addEntry()
这里写图片描述
首先是需要判断需不需要扩容。如果需要就调用resize();然后在重新计算存放的位置。最后调用createEntry()把元素加进去。看看扩容的方法。
resize(2 * table.length)
这里写图片描述
这个方法其实也简单,先判断容量是否已经最大了,最大了就直接返回Integer.MAX_VALUE;
关键的是我们要去看这个transfer()方法。
transfer()
这里写图片描述
先判断是否为null,如果为null,说明没有元素,不需要进行这次的从排序,然后在重新计算元素的下标 ,进行一个重新的排序。然后在指到next。
到了这里要往里面放的工作,已经全部准备好了,接下来我们就要往里面放了。
createEntry(int hash, K key, V value, int bucketIndex)
这里写图片描述
然后size++,到这里我们我put()方法就已经完成了。
给大家总结一下put()方法的流程。
1。判断key是否为null,如果为null,进入putForNullKey()方法进行处理。
2。如果不为空,然后进行一些操作,取到要存的元素的位置,这里面的一个关键点,已经讲过了2的次幂。然后在判断,我们是新增这个元素,还是覆盖,通过equals()进行key的比价,存在覆盖。不存在添加。
3。在添加的时候,进行一次判断,是否需要进行扩容,如果需要就进行扩容,然后从新排序。
4。create一个Entry,往里面添加元素。

我们来看看get()方法
这里写图片描述
先判断是否为null,然后通过getEntry(key);取到这个Entry,在通过Entry取到value;
getEntry(key)
这里写图片描述
从这里我们就可以看到,其实根本的思想是一样的,我们存的时候,是怎么样的,我们取的时候其实也是一样的,也是先通过key.hashCode()算出hash,然后在通过indexFor()算出存的下标。然后我们在循环。关键看这个if()看看他是怎么判断key的。首先保证key的hash。
((k = e.key) == key || (key != null && key.equals(k)))这个其实就是因为可能是数字或字符串,所以做的处理,就是比较key。然后我们就取出这个Entry了。然后在通过getValue()就取到值了。

getForNullKey()
这里写图片描述
就是一个循环,判断key==null,就把他的value给返回。

我们来看看remove()方法
这里写图片描述
看到这里我们又要去看看removeEntryForKey()方法了。

removeEntryForKey()
这里写图片描述
这里面通过一系列的判断,指针移动,达到移除的目的。

虽然hashMap的方法,没有全部讲完,但是通过这几个方法,我们已经可以大致的全部了解。

大家还记得modCount这个属性?在很多方法调用的时候modCount++就会增加。如果比较expectedModCount!=modCount,就会快速失败。减小损失。会抛出这个异常throw new ConcurrentModificationException();

有机会下次分析这个异常。