? 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,本文会对java集合框架中HashMap的实现原理进行讲解,并对JDK1.8的HashMap源码进行分析。
面试官的灵魂拷问??
谈一下HashMap的特性?
HashMap中hash函数是怎么实现的?
为什么HashMap数组的长度必须是2的N次幂?
HashMap的扩容机制是什么?
为什么红黑树和链表的转换阈值是6和8不是7呢?
为什么jdk1.8中新增的是红黑树而不是AVL?
什么是HashMap?
???HashMap是基于哈希表的Map接口的非同步实现,是一种用于key-value的映射关系的集合,并且key和value都允许为null,且不保证顺序性。在1.7的时候底层采用数组+链表实现,1.8底层采用数组+链表+红黑树实现(以下基于1.8)。
数据结构
?
基本常量
//默认初始化大小?1<<4=16
static?final?int?DEFAULT_INITIAL_CAPACITY?=?1?<<?4;?//?aka?16
//最大容量
static?final?int?MAXIMUM_CAPACITY?=?1?<<?30;
//加载因子0.75
static?final?float?DEFAULT_LOAD_FACTOR?=?0.75f;
//当链表长度大于等于该值时转换为红黑树
static?final?int?TREEIFY_THRESHOLD?=?8;
//当红黑树的大小小于等于该值时转换回链表
static?final?int?UNTREEIFY_THRESHOLD?=?6;
//转换为红黑树时当前整个数组的大小必须大于等于这个值
//才进行转换,可以去看看转换代码treeifyBin
static?final?int?MIN_TREEIFY_CAPACITY?=?64;
构造函数
?public?HashMap(int?initialCapacity,?float?loadFactor)?{
????????if?(initialCapacity?<?0)
????????????throw?new?IllegalArgumentException("Illegal?initial?capacity:?"?+
???????????????????????????????????????????????initialCapacity);
????????if?(initialCapacity?>?MAXIMUM_CAPACITY)
????????????initialCapacity?=?MAXIMUM_CAPACITY;
????????if?(loadFactor?<=?0?||?Float.isNaN(loadFactor))
????????????throw?new?IllegalArgumentException("Illegal?load?factor:?"?+
???????????????????????????????????????????????loadFactor);
????????//设置加载因子
????????this.loadFactor?=?loadFactor;
????????//计算出下次扩容的阈值
????????this.threshold?=?tableSizeFor(initialCapacity);
????}
???//可传入初始容量
????public?HashMap(int?initialCapacity)?{
????????this(initialCapacity,?DEFAULT_LOAD_FACTOR);
????}
????public?HashMap()?{
?????//?all?other?fields?defaulted?0.75f
????????this.loadFactor?=?DEFAULT_LOAD_FACTOR;?
????}
上面三个构造函数中第二个函数调用调用的是第一个构造函数,第三个是默认的无参数构造。下面主要讲第一个。
在构造函数HashMap(int initialCapacity, float loadFactor)中,前面对两个参数进行了范围验证,然后设置加载因子,并且结算下一次扩容的阈值,这里看一下源码
static?final?int?tableSizeFor(int?cap)?{
????????int?n?=?cap?-?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;
????}
以上tableSizeFor()这个函数的意思就是返回一个>=给定数的2^N。
>>>表示不带符号向右移动二进制数。
|=表示按二进制位或操作,并赋值等价于a=a|b。
以下
/**
?*
?*?以正数8为例
?*?源码:
?*?00000000?00000000?00000000?00001000
?*?>>>1
?*?00000000?00000000?00000000?00000100
?*?|=1
?*?00000000?00000000?00000000?00001100
?*?>>>2
?*?00000000?00000000?00000000?00000011
?*?|=2
?*?00000000?00000000?00000000?00001111
?*?>>>4
?*?00000000?00000000?00000000?00000000???到这里可以看出来,后面的计算是无效的
?*?|=4
?*?00000000?00000000?00000000?00001111
?*?-------------------------------------------------
?*?以正数7为例
?*?源码:
?*?00000000?00000000?00000000?00000111
?*?>>>1
?*?00000000?00000000?00000000?00000011
?*?|=1
?*?00000000?00000000?00000000?00000111
?*?>>>2
?*?00000000?00000000?00000000?00000001
?*?|=2
?*?00000000?00000000?00000000?00000111
?*/
?
上面整个计算过程的意思就是从高位到低位第一个1后面的数字全部置1。
为什么要把第一个1后面的数字全部置1?因为2在计算机中具有特殊的地位。如果一个数等于2^N,这个数的二进制表示肯定只有一个1(不包括符号位)。
代码一开始为什么要减1?可以看到上面正数8进行5次移位或等的结果是15,与预期初始化长度为8大了一个级别。这里减掉一个1,在进行移位运算的时候将自己本身也包含进去。
前面移位运算的结果总是2^N-1,所以在函数返回的时候+1,刚好等于2^N
hash函数
??static?final?int?hash(Object?key)?{
????????int?h;
????????return?(key?==?null)???0?:
????????(h?=?key.hashCode())?^?(h?>>>?16);
????}
首先进行了>>>16位,也就是将高16位移动到低16位, 然后^按位异或(同为0,不同为1),其运算结果高地位都参与进去了,增加了随机因子。
插入原理
因为put函数调用的直接就是putVal()方法,所以这里直接看putVal(),代码挺长;
putVal()源码注释
final?V?putVal(int?hash,?K?key,?V?value,?boolean?onlyIfAbsent,
???????????????????boolean?evict)?{
????????Node<K,V>[]?tab;?Node<K,V>?p;?int?n,?i;
????????//判断数组是否存在和数组长度
????????if?((tab?=?table)?==?null?||?(n?=?tab.length)?==?0)
????????????//创建新的数组
????????????n?=?(tab?=?resize()).length;
?????????//判断该地址是否存在元素节点,并将p指向地址
????????if?((p?=?tab[i?=?(n?-?1)?&?hash])?==?null)
????????????tab[i]?=?newNode(hash,?key,?value,?null);//创建新的节点
????????else?{
????????????//该下标已存在节点
????????????Node<K,V>?e;?K?k;//声明临时节点
????????????if?(p.hash?==?hash?&&
????????????????((k?=?p.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????????//如果元素的hash相同且equals相等,那么判定为同一个元素
????????????????e?=?p;
????????????//p的引用是否为红黑树
????????????else?if?(p?instanceof?TreeNode)
????????????????e?=?((TreeNode<K,V>)p).putTreeVal(this,?tab,?hash,?key,?value);
????????????//不是同一个节点,也不是红黑树
????????????else?{
????????????????for?(int?binCount?=?0;?;?++binCount)?{
????????????????????//判断并赋值下一个节点
????????????????????if?((e?=?p.next)?==?null)?{
????????????????????????//创建新的节点
????????????????????????p.next?=?newNode(hash,?key,?value,?null);
????????????????????????//?判断是否满足转换为红黑树
????????????????????????if?(binCount?>=?TREEIFY_THRESHOLD?-?1)?
????????????????????????????treeifyBin(tab,?hash);//转换为红黑树
????????????????????????break;
????????????????????}
????????????????????//判断链表内的元素和赋值的元素是否为同一个元素
????????????????????if?(e.hash?==?hash?&&
????????????????????????((k?=?e.key)?==?key?||?(key?!=?null?&&?key.equals(k))))
????????????????????????break;
????????????????????p?=?e;
????????????????}
????????????}
????????????if?(e?!=?null)?{?//?existing?mapping?for?key
????????????????V?oldValue?=?e.value;
????????????????if?(!onlyIfAbsent?||?oldValue?==?null)
????????????????????e.value?=?value;
????????????????//将节点移动到最后
????????????????afterNodeAccess(e);
????????????????return?oldValue;
????????????}
????????}
????????++modCount;//增加修改次数
????????//增加大小,并判断是否扩容
????????if?(++size?>?threshold)
????????????resize();//扩容
????????afterNodeInsertion(evict);
????????return?null;
????}
执行流程图
?
扩容机制
resize()源码
??final?Node<K,V>[]?resize()?{
????????Node<K,V>[]?oldTab?=?table;
????????int?oldCap?=?(oldTab?==?null)???0?:?oldTab.length;
????????int?oldThr?=?threshold;
????????int?newCap,?newThr?=?0;
????????if?(oldCap?>?0)?{
????????????if?(oldCap?>=?MAXIMUM_CAPACITY)?{
????????????????threshold?=?Integer.MAX_VALUE;
????????????????return?oldTab;
????????????}
????????????else?if?((newCap?=?oldCap?<<?1)?<?MAXIMUM_CAPACITY?&&
?????????????????????oldCap?>=?DEFAULT_INITIAL_CAPACITY)
????????????????//扩容一倍?????
????????????????newThr?=?oldThr?<<?1;?//?double?threshold
????????}
????????else?if?(oldThr?>?0)?//?initial?capacity?was?placed?in?threshold
????????????newCap?=?oldThr;
????????else?{???????????????//?zero?initial?threshold?signifies?using?defaults
????????????newCap?=?DEFAULT_INITIAL_CAPACITY;
????????????//这里0.75*数组长度
????????????newThr?=?(int)(DEFAULT_LOAD_FACTOR?*?DEFAULT_INITIAL_CAPACITY);
????????}
????????if?(newThr?==?0)?{
????????????float?ft?=?(float)newCap?*?loadFactor;
????????????newThr?=?(newCap?<?MAXIMUM_CAPACITY?&&?ft?<?(float)MAXIMUM_CAPACITY??
??????????????????????(int)ft?:?Integer.MAX_VALUE);
????????}
????????//设置下一次扩容阈值
????????threshold?=?newThr;
????????@SuppressWarnings({"rawtypes","unchecked"})
????????Node<K,V>[]?newTab?=?(Node<K,V>[])new?Node[newCap];
????????table?=?newTab;
????????if?(oldTab?!=?null)?{
????????????for?(int?j?=?0;?j?<?oldCap;?++j)?{
????????????????Node<K,V>?e;
????????????????//这里开始迁移
????????????????if?((e?=?oldTab[j])?!=?null)?{
????????????????????oldTab[j]?=?null;
????????????????????if?(e.next?==?null)
????????????????????????newTab[e.hash?&?(newCap?-?1)]?=?e;
????????????????????else?if?(e?instanceof?TreeNode)
????????????????????????((TreeNode<K,V>)e).split(this,?newTab,?j,?oldCap);
????????????????????else?{?//?preserve?order
????????????????????????//高低链,高链需要迁移,低链不用
????????????????????????Node<K,V>?loHead?=?null,?loTail?=?null;
????????????????????????Node<K,V>?hiHead?=?null,?hiTail?=?null;
????????????????????????Node<K,V>?next;
????????????????????????do?{
????????????????????????????next?=?e.next;
????????????????????????????if?((e.hash?&?oldCap)?==?0)?{
????????????????????????????????if?(loTail?==?null)
????????????????????????????????????loHead?=?e;
????????????????????????????????else
????????????????????????????????????loTail.next?=?e;
????????????????????????????????loTail?=?e;
????????????????????????????}
????????????????????????????else?{
????????????????????????????????if?(hiTail?==?null)
????????????????????????????????????hiHead?=?e;
????????????????????????????????else
????????????????????????????????????hiTail.next?=?e;
????????????????????????????????hiTail?=?e;
????????????????????????????}
????????????????????????}?while?((e?=?next)?!=?null);
????????????????????????if?(loTail?!=?null)?{
????????????????????????????loTail.next?=?null;
????????????????????????????newTab[j]?=?loHead;
????????????????????????}
????????????????????????if?(hiTail?!=?null)?{
????????????????????????????hiTail.next?=?null;
????????????????????????????newTab[j?+?oldCap]?=?hiHead;
????????????????????????}
????????????????????}
????????????????}
????????????}
????????}
????????return?newTab;
????}
扩容机制
?
扩容时高低链问题
先看代码是如何通过hash定位数组下标的·
????????if?((p?=?tab[i?=?(n?-?1)?&?hash])?==?null)
????????????tab[i]?=?newNode(hash,?key,?value,?null);
首先知道数组的长度永远是2^N次方,转换为2进制,只有1个1,(2^N)-1,二进制从2^N这一位的后一位开始为后面全是1,采用这种方式分布也是可以让不同的hash值定位到相同的下标上面。(注:同一个链表里面的所有节点hash不一定相同,相同hash的肯定定位到同一个链表)
再看看如何区分高链和低链的
???if?((e.hash?&?oldCap)?==?0)?{
???????????if?(loTail?==?null)
????????????????loHead?=?e;
???????????else
????????????????loTail.next?=?e;
????????????????loTail?=?e;
???????????}
????else?{
???????????????if?(hiTail?==?null)
??????????????????hiHead?=?e;
???????????????else
??????????????????hiTail.next?=?e;
???????????????hiTail?=?e;
????}
上面代码就是hash & oldCap时=0时就是低链,!=就是高链,将高链偏移一个原始数组的位置即可。
(注:这里(2^N)-1和(2^N-1)在二进制的时候除了第一位一样,其余位全部不一样,所以偏移一个原始数组位置)
为什么转换阈值不是7?
直接看什么时候转换的
???//插入时
???if?(binCount?>=?TREEIFY_THRESHOLD?-?1)
????????treeifyBin(tab,?hash);
//转换或者移除时,很多地方大差不差
?if?(lc?<=?UNTREEIFY_THRESHOLD)
??????tab[index]?=?loHead.untreeify(map);
转换红黑树条件>=7,转换回来条件<=6,插入的时候是已经存在了7个,当前插入的算一个,相当于间隔了一个的。如果是7的话,那么链表和红黑树之间的切换范围值就太小了。如果我的链表长度不停地在7和8之间切换,那岂不是得来回变换形态?所以选择6是一种折中的考虑。
为什么jdk1.8选用红黑树?
AVL树性质
本身首先是一棵二叉搜索树。
带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子)最多为1。也就是说,AVL树,本质上是带了平衡功能的二叉查找树(二叉排序树,二叉搜索树)
红黑树性质
每个节点,不是红色就是黑色的;
根节点是黑色的;
如果一个节点是红色的,则它的两个子节点是黑色的(没有连续的红色结点,可以有连续的黑色节点,这里意味着最长路径最差情况是最短路径的1倍)
对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。(每条路径的黑色结点个数相等)
每个叶子节点都是黑色的(这里的叶子节点是指的NIL节点(空节点)) 优缺点:因为不要求严格平衡,在查询效率上比AVL少许,但是在插入效率上比AVL效率高,这里增删改差的效率比较平衡;
整体来说:AVL树侧重于搜索,红黑树因为不要求严格彭亨,在插入和搜索两个功能上性能比较均匀,所以选用红黑树。
其他常见问题
Q:什么是哈希碰撞?
A:HashMap中2个对象产生相同的hashcode时就是哈希碰撞
Q:HashMap是如何解决哈希碰撞?
A:产生哈希碰撞的对象,以链表的形式存储在数组的同一个位置上
Q:HashMap的数组中同一链表上对象的hashcode都一样的吗?
A:不是,而且大多数情况下都不一样