HashMap原理及面试题
hashMap 类图
图1
hashMap数据结构图
图2(注:该图实在网络上找的)
hashMap 变量初始值
// 默认的初始容量(必须是2的幂数)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 树化临界值
static final int TREEIFY_THRESHOLD = 8;
// 取消树化临界值
static final int UNTREEIFY_THRESHOLD = 6;
// 容器可以树状化的最小表容量。(应该至少是4 * TREEIFY_THRESHOLD)
static final int MIN_TREEIFY_CAPACITY = 64;
// 要调整大小的下一个大小值(容量×负载因子)。
int threshold;
实现逻辑
- 实例化时,可以指定初始容量和负载因子,将初始容量赋给threshold;
- put逻辑:
2.1 若数组为空则初始化数组;
2.2 计算key的hash值在数组中的位置,该位置若无值,则存放到该位置;
2.3 若该位置有值,且为树节点,则将put的数据加入树中;
2.4 若该位置有值,且不为树节点,则将put的数据加入链表的最后,并判断该链表是否达到树化条件;
2.5 若达到树化条件,则将链表转化为红黑树 - get逻辑:
3.1 计算入参key的hash,取数组中该位置的值;
3.2 判断该位置的值的key是否为入参的key,若不是,则执行以下步骤;
3.2 若该值为树节点,则遍历红黑树,取相应的树节点;
3.3 若该节点不为树节点,则遍历该链表,取与key相等的node; - remove逻辑:
4.1 获取入参key所对应的Node节点;
4.2 若Node为树节点,则调用node.removeTreeNode(this, tab, movable);移除树节点;
4.3 若Node在数组中,则直接将node位置置空,若在链表中则将node的下个节点指针赋给上个节点;
4.4 size做自减操作; - clear清空map逻辑:
5.1 将size置0;
5.2 遍历数组,将数组的每个位置置空; - resize扩容逻辑:resize方法主要分为两部分,一是数组的扩容,二是将迁移原数组的数据
6.1 数组容量默认为16,最大为1>>30,负载因子默认为0.75,
6.2 扩容的新数组是原数组的容量的2倍,并且下次扩容的阈值为新数组容量×负载因子;
6.3 迁移原数据:结点在新数组中的位置只有两种,原下标位置或原下标+旧数组的大小;
6.4 扩容时可能将树转换为链表。 - hash实现:
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
通过key的hashCode()的高16位去异或低16位;因为表使用的是2次幂掩码,因此这样做可以减少碰撞。
面试常问
1. HashMap数据结构?
在jdk8中,HashMap的数据结构变为数组+单链表+红黑树的结构,如图2;
并且单链表使用的是尾插法。
2. HashMap工作原理?
调用hash方法计算key的hash以确定在数组中的位置,若该位置不存在值,则插入到该位置;若该位置存在值,即发生碰撞,即判断该位置的节点是否为树节点,若为树节点,则调用添加树节点方法,若不为树节点,则遍历单链表,比对插入的key是否存在,若存在则替换value的值,若不存在则插入到单链表的尾部,若单链表的长度大于等于8则将链表转化为红黑树,最后size自加,并判断数组是否需要扩容,需要则调用resize方法进行扩容。
3. HashMap的数组扩容实现?
数组扩容分为两部分,第一部分是确定扩容后新数组的长度以及下次扩容的阈值,第二部分是将原数组中的值迁移到新的数组;
计算扩容数组的长度,新数组的长度扩容到原数组的长度的二倍,但不能超过2的30次幂;
下次扩容的阈值为新数组长度乘以负载因子,并且不能大于Integer的最大值;
迁移原数据,遍历原数组,重新计算节点在新数组的的位置,若是单链表则遍历单链表,若是 树节点,则调用树的拆分方法,将树拆分为上部和下部,若是树小于6则取消树化。