您的位置:首页 > 其它

HashMap源码分析

2017-12-21 18:23 225 查看
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();

有机会下次分析这个异常。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: