内部数据结构
JDK1.8为例,内部使用数组+链表/红黑树的实现方式
学过数据结构的都知道拉链法构造哈希表,这就是通过拉链法构造的过程。
但是也有区别,当因哈希碰撞形成的链表长度超过8且数组大小超过64(包含64)时,会变成红黑树,提高查找的效率。
当红黑树节点小于6时,又会转化回链表。
数据插入原理
1.判断数组是否为空,若为空则初始化数组,否则,通过计算(n-1)&hash值得到存储的位置。
2.通过查看计算出的位置是否已经存在数据判定是否发生了hash冲突,如果没有,则存放节点,若有,则比较原本的key,如果相等,则覆盖并返回旧的值,如果不相等,则判定该节点是否是红黑树节点,如果是,则插入树,如果不是则插入链表,同时判定插入链表后是否长度超过了8,如果超过了则转换为红黑树。
3.当上述操作完成后再判断数组中节点数是否超过了阈值,如果超过则进行扩容。扩容操作为左移一位,即变两倍。
初始化设置
当调用无参构造方法时,HashMap会使用默认设置,默认大小为16,负载因子是0.75。
当调用有参构造方法时,如new HashMap(a),则大小为>=a的2的整数次幂。
具体计算方式为
int n=a-1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n<0)?1:(n>=MAXIMUM_CAPACITY)?MAXIMUM_CAPACITY:n+1;
这样可能有点抽象,通俗的说就是
将这个数二进制右移,并与自身异或,将第一个1不断右移,即所有位全部变成1后再加1,即得到了第一位是1,后面全是0的二进制数,这样的数就是2的整数次幂。
HashMap中的哈希函数
哈希函数先拿到key的hashcode(Java所有类都继承了Object类,hashcode默认情况下是它的内存地址,为32位的int值),然后将hashcode的前16位与后16位进行异或。又称扰动函数。
如此设计的原因:可以减少哈希碰撞发生次数,位运算效率比较高。
详细解释:上面提到,散列值是一个32位的int类型数,映射空间过于庞大,大约有40亿,内存不可能存放,并且如果采用常见的取模哈希函数,效率很低。因此使用n-1&hashcode的方法来进行计算,可以把第一个非0高位1后面的所有数截取出来,从而提高了效率。
但是若只是采用这种方式得到的存放位置,碰撞现象是很容易出现的,就算不出现,也会造成入碎片状的内存,因此需要扰动函数。
通过将前后16位进行异或,可以让前后信息都存在,从而更容易将数与数之间区分开。
HashMap的优化
1.扰动函数只进行1次。(实际证明,1次就够了)
2.加入了红黑树,链表长度超过8且数组长度超过64时会转化为红黑树,红黑树节点少于6时则会转换为链表。(降低复杂度)
3.链表的插入方式从头插法改为了尾插法。(头插法容易在多线程环境形成环,扩容情况下链表反转)
4.改为先插入再扩容。(提高效率)
5.不再需要重hash就可以确认位置。(提高效率)
是否支持同步
不支持同步。若要支持同步,可以使用synchronizedMap方法,Hashtable和ConcurrentHashMap来替代HashMap。
Hashtable简单使用的synchronized锁,效率很低,同时也不支持空值,而HashMap支持Null。
而synchronizedMap方法使用对象锁。
ConcurrentHashMap的话1.7以前使用的是分段锁,提高并发量,1.8后使用加入了synchronized和CAS(compare and swap)。
所谓分段锁就是将数据分段,彼此互不影响,具体实现即将成员变量用volatile修饰(补充:volatile两个作用,一是保证变量透明,二是防止指令重排,所谓变量透明即每次访问这个变量时要从主存读取,不能从内存读取,防止多线程操作下数据不一致的问题。)然后使用CAS和synchronized实现赋值(CAS简单说就是将预期值和实际值比较若相同则直接进行交换,无需再次从主存访问)。
哈希碰撞发生时的链树转换阈值
链表变成红黑树需要长度超过8,红黑树变回链表则需要节点少于6.
hash碰撞8次的概率很低,所以设置8为界限,但是如果少于8就将红黑树变成链表,会出现一个问题,反复发生转换造成效率低下,因此设置为6.
有序性
HashMap是无序的,若需要有序的HashMap,可以使用LinkedHashMap和TreeMap
LinkedHashMap
底层实现是一个双节点的单链表,节点继承HashMap的节点,并拥有前后节点的标记,因此可以进行排序。
TreeMap
底层实现是红黑树,可以通过实现Comparable接口也可也实现Comparator比较器来定义一个排序方法。进行排序。