HashMap(Java8以前):数组+链表

HashMap(Java8及以后):数组+链表+红黑树

 

 

如果存入的key是一个自定义的类,该怎么办呢?(重新equals和hashcode)

进入源码:

可以看到key是由Set来存的,就不能够有重复数据了

values用Collection来存,可以重复

内部存储结构是[是一个MapEntry的实现]

数组被分为一个个的bucket,通过hash值决定了键值对在这个数组的寻址,hash值相同的键值对则以链表的方式存储

 

如果链表的大小超过了一定数值,默认8,则会转化为红黑树

当链表的大小小于一定数值,默认6,则会由红黑树转换回链表

再看HashMap 的构造函数,可以发现它仅仅只是为一些相关的成员变量赋上了初始化值,所以可以大胆猜测:HashMap的加载是懒加载(lazy-load的模式)

接下来看它的put方法

调用putVal,点进去,发现最初几行调用resize()方法

然后回到putVal方法,继续分析,这一步是得到经过hash之后的值来确定位置,如果Node<K,V>[] tab这里面没有,就新建一个,放进去

如果发现在tab[i]中存在键值对,并且key和传进来的key相同,就直接替换数组中的元素

然后再判断是否是树化后的节点,如果是的话,就按照树的方式尝试存储键值对进行存储

如果都不是,那么就直接在链表后面新建一个,但是如果binCount超过了阈值,那么就进行树化

 

总结HashMap的put方法逻辑:

1.如果HashMap未被初始化过,则初始化(也就是进行resize()操作)

2.对key进行一个hash操作,并且和数组长度减一后进行与操作,计算出下标

3.如果没有碰撞,就直接放入桶中

4.如果碰撞了,就以链表的方式链接到后面(拉链法

5.如果链表长度超过阈值,就把链表转成红黑树

6.如果链表长度低于6,就把红黑树转回链表

7.如果节点已经存在,就替换旧值

8.如果桶满了容量16*加载因子0.75),就需要resize扩容2倍后重排

 

HashMap如何有效减少碰撞?

扰动函数:促使位置分布均匀,减少碰撞几率

使用final对象,并且采用合适的equals()和hashCode()方法

 

HashMap中hash值的求法

先取到key的hashCode,再在其基础上进行移位,将高16位移动到低16位上面,再与原先的数据进行异或运算

这样做的好处是什么?

第二步能够综合高16位和低16位的数据情况,以此加大低位的随机性,而且低位也相当于变相保存了高位信息。主要是从速度、质量、功效来考虑的

第三步中,(n - 1)相当于是桶(或者数组)的长度【?】,这一步的运算就相当于是用hash值与长度取模,但是这个效率更加快

[n–1的原因是:因为初始状态是2的n次方,如果减一个1的话,它的位数就正好表示n钟情况,比如16-1就15,15的话就是0000 1111,有4位,2的4次方,就是16钟情况了,刚好可以把他们放到16个桶里面去]

更快的原因是由于hashMap的长度是2的n次方,那为什么是2的n次方而不是传入的长度呢?

其实在传入初始化capacity(容量)值的时候,还额外调用了一个方法:tableSizeFor

它的作用其实就相当于是将传入的参数转换成离它最近的2的倍数的值

为了方便在进行hash运算定位桶的时候能实现用上述的与操作,代替取模,从而获得更好的效果.

这儿相当于一道非常漂亮的算法题:输入一个int类型的数,返回比某个2的倍数大且最接近的那个数

附: 

按位或后赋值   |=   

无符号右移     >>>

 

为什么不直接用获取到的key.hashCode()来访问数组呢?

因为hashCode()方法返回的是int类型,而其范围是-2147483648~+2147483647(即正负2的16次方),然而一个40亿长的数组,内存是不容易存储的,更何况默认的hashMap在扩容之前大小才16,所以直接拿散列值来用是不现实的

 

HashMap是如何扩容的?

首先了解一个变量DEFAULT_LOAD_FACTOR(默认负载因子),意思是当一个HashMap填满了75%的bucket时,就会自动扩容

threshold 阈值 

原理是通过创建原来HashMap大小x2的bucket数组,来重新调整map的大小

并将原来的对象放入到新的bucket数组中去(这个过程叫做rehashing,这个算法挺有意思的,挖个坑,有心情就来填)

 

HashMap扩容会存在哪些问题?

多线程环境下,调整大小会存在条件竞争,容易造成死锁

rehashing是一个比较耗时的过程

 

如何让HashMap转为线程安全?

可以通过

让hashMap线程安全。其原因是添加了一个互斥对象成员,对里面的public方法使用synchronized进行加锁

效果上和HashTable差不多

为什么HashMap线程不安全?

可能造成Bucket中链表形成环形链表,导致后续的Map.get操作可能会造成无限循环,导致CPU的100%占用。