扫盲细节,关于HashMap,你不能只知道put和get
HashMap基本概念及原理:
如果我们想要很快的查询一个数据,最好将其用数组存储,因为数组查询速度快,但是数组的长度不可以修改,所以它添加元素很麻烦,需要创建一个更大的数组,然后把老数组的元素按顺序拷贝到新数组中,而我们想要添加元素,最好使用链表去存储,因为链表是离散的,所以在添加或者删除的时候,只会修改局部的内容,也正是因为链表是离散的,它的位置在内存中不是一直固定的(指的是不连续),每次要查找下一个元素的时候,都需要读取其位置信息,所以链表的查询很慢。那有没有一种数据结构,它的查询很快,添加和删除速度也很快呢?答案是肯定的,结合数组和链表的优点,哈希表诞生了。
HashMap基于哈希表的Map接口实现,是以key-value的存储形式存在,即主要用来存放键值对。HashMap的实现不是同步的,这意味着它不是线程安全的。数组是HashMap的主体,链表则是为了解决hash冲突而存在的,所谓hash冲突就是两个对象调用hashCode()方法计算的hash值相同导致计算的数组索引也相同。
JDK1.8之后在解决Hash冲突时有了较大的变化,当链表长度大于边界值(默认为8)且当前数组长度大于64时,此时此索引位置上的所有数据改为使用红黑树存储。另外需要注意的是,当链表长度大于8但是数组长度小于64,此时也并不会将链表变成红黑树,而是选择扩容。这样做的目的是提高性能和较少搜索时间,具体可参照treeifyBin()方法。说了这么多,那HashMap的基本原理是怎样的呢?简单粗暴概括一下:
1、首先判断key是否为Null,如果为null,直接查找Enrty[0],如果不是Null,先计算Key的HashCode,得到Hash值,Hash值是一个int值。
2、根据Hash值,要找到对应的数组,所以对Entry[]的长度length取模(类似求余的算法,后面详细介绍),得到的就是Entry数组的index。
3、找到对应的数组就找到了所在的链表,然后按照链表的操作对Value进行插入、删除和查询操作。
HashMap底层数据结构及存储过程(以上纯属扯淡,下面重点来了):
JDK1.8之前HashMap底层由数组+链表实现
JDK1.8之后为了提高效率,底层由数组+链表+红黑树实现
在创建HashMap集合对象的时候,在JDK1.8之前是在构造方法中创建一个长度为16的Entry[] table来存储键值对,在JDK1.8之后不在构造方法中创建数组了,而是在第一次调用put()方法时创建数组Node[] table 用来存储键值对。
假设向哈希表中存储键值对key为zhangsan,value为28,根据zhangsan.hashCode()方法计算出hash值,然后结合数组长度采用取模的算法计算出zhangsan在Node数组中的索引值,如果计算出的索引没有值,则直接将28存储到数组中。那么,取模算法到底是怎样的呢?看下图。
红色框出来的代码告诉我们,采用的是按位与运算计算出索引值,其实就是我们熟知的取余法,但是为什么没有直接使用hash%length直接取余呢,是因为与运算效率更高,与运算规则:相同的二进制数位上都是1时结果为1,否则为0。在某种条件下hash%length等于n-1&hash,什么条件呢?那就是HashMap要求的数组长度length必须为2的n次幂,HashMap的构造函数允许我们自定义数组长度,但是它会检测然后自动帮我们把设置的长度往上转成最近的2的n次幂,比如我们初始化一个HashMap对象,设置数组长度为10,显然10不是2的某次幂,这时候会自动向上转成最近的2的某次幂,也就是16。
HashMap<String,String> hashMap = new HashMap<>(10);复制代码
以下疑问:
1、默认加载因子为什么是0.75?
加载因子表示达到length的阈值就扩容,也就是节点数超过16*0.75就会扩容,扩容是新建一个length为原来2倍的数组,并将原数据移过去。
2、关于HashMap优化:尽量设置初始化容量
《阿里巴巴Java开发手册》中建议初始化时设置HashMap的初始化容量。那到底设置成多少合适呢?
3、为什么节点数超过8就会有链表转成红黑树?
节点存储数据的概率服从泊松分布,0对应的0.60653066表示数组中index为0的空间存储数据的概率为0.60653066,index为8的空间存储数据的概率为0.0000006,这就不难解释为什么节点数超过8就会有链接转红黑树了,这也是为了解决hash冲突。