HashMap是我们日常开发中几乎每天都要用到的集合类,它是以k-v的方式进行存储的。在jdk1.7到jdk1.8HashMap的实现略有不同,其中两个重要的区别是在1.7中HashMap采用的数据结构是数组+链表,但是到了1.8中HashMap采用数组+链表+红黑树。红黑树的引用是为了提高它的查询效率,因为链表查询的时间复杂度是O(n),而红黑树的时间复杂度是O(log(n))。还有一个就是当我们发生Hash碰撞时1.7采用头插法,而1.8采用为尾插法。当然1.7到1.8还有很多优化的细节,比如对它的hash算法进行简化。
接下来我就以jdk1.8来和你聊一聊HashMap的基本实现原理。首先在我们创建HashMap时,阿里规约里要求我们传入一个初始化容量,就是说在我们预知将来要插入多少个数据的情况下最好传入一个初始化容量,而且这个容量最好是2的次幂。当然如果不传入初始化容量,HashMap的默认容量是16.
接下来我会和你聊聊为什么是2的次幂。首先当我们往HashMap里put值的时候,当put第一个值时,HashMap的数组会被初始化。按照我们传入的初始化容量,大于等于这个初始化容量的最近的一个2的次幂进行初始化数组。初始化之后他会用key的hash值 & (容量 - 1)算出他的下标。key的hash算法用了扰动函数
(h = key.hashCode()) ^ (h >>> 16) 这样可以使高16位与低16位运算,增加了随机性可以减少hash冲突。为什么我们之所以采用与运算而不是取模,因为与运算在计算机中的效率非常高,所以HashMap采用与运算的方式 。
当然在我们添加数据的时候会发生避免不了的两个问题,一个就是扩容的问题,另一个就是树化的问题。
关于扩容的问题,在HashMap中有一个成员变量,它叫做负载因子默认是0.75。当我们HashMap的size也就是插入节点的数量大于等于HashMap的容量 * 负载因子时就会进行扩容。扩容的方式是 * 2,这样也保证了容量是2的次幂。
当我们一条链表上的数据足够多的时候,就可能会进行树化。树化的前提就是链表上的节点大于等于8个,当然只有这一个条件还不够,在HashMap的源码中还有个成员变量,就是最小的树化容量,默认是64。也就是我们数组的容量达不到64的时候他会优先选择扩容而不是对链表进行树化。所以树化有两个先决条件,一个是容量大于等于64,另一个就是一条链表上的节点大于等于8.当节点小于等于6的时候会把树转为链表。
树化和扩容都比较耗性能。所以在阿里规约中要求我们初始化容量的根本目的就是文为了让我们减少扩容的次数。阿里规约中给了一个初始化容量的计算的方式,我记得应该是要存入数据的数量除以负载因子然后再加一。