文章目录

  • 1 类图
  • 2 属性
    • 2.1 常见属性介绍
    • 2.2 底层数据结构
    • 2.3 重要常量
    • 2.4 链表和树的问题
      • 1 为什么不直接采用红黑树?
      • 2 为什么采用红黑树,而不是AVL平衡树?
  • 3 构造器
    • 3.1 常用构造器
  • 4 put方法
    • 4.1 put操作的整体思路
    • 4.2 树操作(略)
  • 5 get方法
  • 6 哈希处理
    • 6.1 如何有效较少碰撞
    • 6.2 hash的实现
    • 6.2 HashMap底层数组为什么总是2的n次方?
  • 7 扩容resize
    • 7.1 代码分析
    • 7.2 扩容的问题
  • 源码分析地址
  • 参考资料

1 类图

HashMap是非常常用的工具类,实现Map接口,存储key-value键值对,功能与HashTable类似,但是线程不安全,允许空键和空值。

2 属性

2.1 常见属性介绍

// 存放数据的数组
transient Node<K,V>[] table;// 缓存entrySet()值,用于keySet()和values()
transient Set<Map.Entry<K,V>> entrySet;// map中键值对个数
transient int size;// HashMap结构修改的次数,用于快速失败
transient int modCount;//下次resize操作的阈值,值等于capacity * load factor
//此外,数组还未分配时,本字段存放初始数组容量,0表示DEFAULT_INITIAL_CAPACITY
int threshold;// 负载因子--由final修饰,必须在构造器中初始化。
final float loadFactor;

初始容量和载入因子影响HashMap性能。容量即桶的数量,初始容量就是Node数组初始大小。载入因子是哈希表有多满的度量,当map中Entry个数大于当前容量与载入因子乘积时,进行rehash操作。

loadFactor值默认为0.75,是均衡时间和空间成本的折中值。loadFactor高会较少空间开销(扩容次数减少),但是增加了查询成本(因为hash冲突变长,导致桶中Node较多)。

2.2 底层数据结构

HashMap底层采用的数据结构是数组、链表和红黑树。数组元素(称为桶)可能是链表,也可能是红黑树,对于null和单个node可以视为是链表。链表长度大于等于8(TREEIFY_THRESHOLD)并且数组容量大于64时,转化为红黑树;红黑树元素数小于等于6(UNTREEIFY_THRESHOLD)时,转化为链表。

![在这里插入图片描述](https://img-blog.csdnimg.cn/20200909234736156.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0xJWkhPTkdQSU5HMDA=,size_16,color_FFFFFF,t_70#pic_center,width=“50%” height=“50%”)

HashMap在Java8之前底层结构是数组+链表结构,我们知道数组查询快、增删慢,而链表增删快、查询慢,实现整合了数组和链表的优点;同时是非线程安全的,查询速率快。

当hash位运算后总是得到同一个值,会使得某个桶链表很长。链表查找是从头遍历,因此HashMap最坏时间复杂度是O(n)。为了解决掉这个问题,Java8设置TREEIFY_THRESHOLD(树化)值,超过阈值便转为红黑树,最坏时间复杂度降低为O(logn)。

2.3 重要常量

// 默认的初始化容量--必须是2的次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16// 最大容量 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;// 默认负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;// 链表长度树化的最小长度
static final int TREEIFY_THRESHOLD = 8;// 树桶元素个数小于等于6,转化为链表
static final int UNTREEIFY_THRESHOLD = 6;// 数组容量大于等于64时,才允许链表转化为红黑树,否则对table数组进行resize操作。
static final int MIN_TREEIFY_CAPACITY = 64;

2.4 链表和树的问题

1 为什么不直接采用红黑树?

因为红黑树需要进行左旋,右旋操作, 而单链表不需要,以下都是单链表与红黑树结构对比。
如果元素小于8个,查询成本高,新增成本低
如果元素大于8个,查询成本低,新增成本高

因为红黑树的平均查找长度是log(n),长度为8的时候,平均查找长度为3,如果继续使用链表,平均查找长度为8/2=4,这才有转换为树的必要。链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

还有选择6和8,中间有个差值7可以有效防止链表和树频繁转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

2 为什么采用红黑树,而不是AVL平衡树?

主要数据结构的差别。

(1)AVL树是更加严格的平衡,因此可以提供更快的查找速度,一般读取查找密集型任务,适用AVL树。
(2)红黑树更适合于插入修改密集型任务。
(3)通常,AVL树的旋转比红黑树的旋转更加难以平衡和调试。

3 构造器

3.1 常用构造器

我们主要介绍默认构造器和map参数构造器,实际使用很少修改loadFactor值。

public HashMap() {this.loadFactor = DEFAULT_LOAD_FACTOR;
}

无参构造器中只初始化了loadFactor,也就是说,使用了懒加载,在添加元素时初始化数组。

public HashMap(Map<? extends K, ? extends V> m) {// 设置负载因子this.loadFactor = DEFAULT_LOAD_FACTOR;// 批量添加元素putMapEntries(m, false);
}final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {int s = m.size();if (s > 0) {// 若是在构造器中调用if (table == null) { // 计算满足负载因子的最小容量float ft = ((float)s / loadFactor) + 1.0F; int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);if (t > threshold)// 取大于阈值的2的n次幂作为阈值threshold = tableSizeFor(t); }// 若是在map.putAll()中调用else if (s > threshold)  resize(); // 逐个放入元素for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {K key = e.getKey();V value = e.getValue();// put操作putVal(hash(key), key, value, false, evict);}}
}// jdk8
// 获取大于cap的最小2的n次幂
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;
}

使用现有map创建新HashMap,主要分为两步:(1)计算阈值;(2)迭代元素处理。

  • threshold的处理:

    • 此处采用tableSizeFor()方法来计算阈值,得到大于并最接近初始容量的2的n次幂值;
    • 后续的阈值则由capacity * loadFactor计算得到。
  • 构造器只是初始化loadFactor和计算阈值,延时初始化。

4 put方法

4.1 put操作的整体思路

步骤总结:

  1. 计算hash,进行put操作
  2. 如果数组为null或者空数组,直接resize,进行初始扩容,得到一个容量为16,阈值为12的Node数组。
  3. 计算索引位置,存入值到数组
    1. 如果索引位置处Node为null,直接初始化新Node;
    2. 如果不为空,则可能存在hash冲突
      1. 若索引位置节点key与新key的hash和值相等,则直接替换。
      2. 若为红黑树,以红黑树形式新增。
      3. 若为链表,自旋判断替换旧节点,还是添加新节点到尾部。
      4. 若找到老节点,进行只替换,返回老值。
  4. 进行到这一步,说明存在新增节点,调整modCount。
  5. 判断阈值,是否需要扩容操作(resize)。
public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}
// 入参 hash:通过 hash 算法计算出来的值。
// 入参 onlyIfAbsent:false 表示即使 key 已经存在了,仍然会用新值覆盖原来的值,默认为 false
// 入参 evict:false表示table是创造器初始化模式
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K, V>[] tab; // 数组Node<K, V> p; // i位置节点int n, i; // n:数组长度;i:索引位置//如果数组为空,使用 resize 方法初始化if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 如果当前索引位置是空的,直接生成新的节点在当前索引位置上if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else { // 索引位置节点不为空,可能存在hash冲突Node<K, V> e; // 存放老节点K k;// 如果 key 的 hash 和值都相等,直接把当前下标位置的 Node 值赋值给临时变量if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = 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);// 当链表的长度大于等于 8 时,链表转红黑树(内部会判断数组大小)if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(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 keyV oldValue = e.value;// 当 onlyIfAbsent 为 false 时,才会覆盖值if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount; // 更新结构修改次数//如果 HashMap 的实际大小大于扩容的门槛,开始扩容if (++size > threshold)resize();afterNodeInsertion(evict);return null;
}

4.2 树操作(略)

5 get方法

理解了put方法后,get方法浅显易懂。

public V get(Object key) {Node<K,V> e;// hash(key) 哈希值return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;// 查找 hash 对应 table 位置的 p 节点if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 如果找到的 first 节点,就是要找的,则则直接使用即可if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;if ((e = first.next) != null) {// 如果找到的 first 节点,是红黑树 Node 节点,则直接在红黑树中查找if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);// 如果找到的 e 是 Node 节点,则说明是链表,需要遍历查找do {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null;
}

注意:

  • get方法得到的值是null,除了不包含key,也可能是原本值就是null的情况。可以通过containsKey判断。

6 哈希处理

6.1 如何有效较少碰撞

HashMap通过树化(treeify)被动提升性能,hash元素也是提升性能的关键。方法主要有两个:

(1)使用扰动函数:不同的对象生成不同的hashcode,促使位置分布均匀,减少碰撞。

(2)使用final对象,并采用合适的hashcode()和equals()方法。final对象hashcode不会改变,并且通常会缓存hashcode值,例如String、Integer。

6.2 hash的实现

static final int hash(Object key) {int h;// h = key.hashCode() 计算哈希值// ^ (h >>> 16) 高 16 位与自身进行异或计算,保证计算出来的 hash 更加离散return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

hash过程,取hashcode值,高位右移16位,然后异或。

右移16位与异或的目的是,混合原始hashcode值的高位与低位,以此加大低位的随机性,同时也掺杂了部分高位特征,使散列结果更加均匀。

6.2 HashMap底层数组为什么总是2的n次方?

HashMap基于桶思想,为了存取高效,要尽量较少碰撞,就是要尽量把每个桶的数据分配均匀;

将数据分配到哪个桶的算法,就是取模,即hash%length。由于在计算机内部取余效率不如位运算,HashMap源码中将其优化为hash&(length-1),hash%length==hash&(length-1)的前提是length等于2的n次方。

为什么这样能均匀分布减少碰撞呢?2的n次方实际就是1后面n个0,2的n次方-1 实际就是n个1;
例如长度为9时候,3&(9-1)=0 2&(9-1)=0 ,都在0上,碰撞了;
例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不同位置上,不碰撞;

7 扩容resize

7.1 代码分析

final Node<K, V>[] resize() {Node<K, V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;// 1. 计算扩容后的新容量if (oldCap > 0) { // oldCap 大于 0 ,说明 table 非空// 超过最大容量,则直接设置 threshold 阀值为 Integer.MAX_VALUE ,不再允许扩容if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)newThr = oldThr << 1; // 扩容一倍} else if (oldThr > 0) // 初始容量被设置在threshold字段(即非默认构造器)newCap = oldThr;else {               // 初始threashold为0,表示默认构造器,采用默认值初始化数组newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}// 2. 上述逻辑中,未计算新阈值的情况,采用newCap * loadFactor 作为新的阀值if (newThr == 0) { // 看上去,也就是非默认构造float ft = (float) newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?(int) ft : Integer.MAX_VALUE);}threshold = newThr; // 将 newThr 赋值给 threshold 属性// 3. 数据迁移@SuppressWarnings({"rawtypes", "unchecked"})Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap]; // 创建新数组table = newTab; // table指向新数组if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K, V> e; // 当前节点if ((e = oldTab[j]) != null) {oldTab[j] = null; // 置空,便于GC// 桶内只有一个节点,直接放到新tableif (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// HashMap 是成倍扩容,这样原来位置的链表的节点们,会被分散到新的 table 的两个位置中去// 通过 e.hash & oldCap 计算,根据结果分到高位、和低位的位置中。// 1. 如果结果为 0 时,则放置到低位// 2. 如果结果非 1 时,则放置到高位// 举个例子,数组大小是 8 ,在数组索引位置是 1 的地方挂着两个值,两个值的 hashcode 是9和33。// 当数组发生扩容时,新数组的大小是 16,此时 hashcode 是 33 的值计算出来的数组索引位置仍然是 1Node<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;elseloTail.next = e;loTail = e;} else { // 满足高位if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);// 设置低位到新的 newTab 的 j 位置上if (loTail != null) {loTail.next = null;newTab[j] = loHead;}// 设置高位到新的 newTab 的 j + oldCap 位置上if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;
}
  1. 如果是初始化,使用threshold值分配初始容量
  2. 否则,扩展2的次幂扩展。每个桶或者留在原位置,或者移动到高位倍位置上

7.2 扩容的问题

(1)当多个线程同时发现hashMap需要调整大小,容易导致条件竞争,进而死锁。

(2)rehash操作是耗时操作。

源码分析地址

Git源码地址

参考资料

  1. https://github.com/luanqiu/java8/blob/master/src/main/java/java/util/HashMap.java
  2. http://svip.iocoder.cn/JDK/Collection-HashMap/

HashMap底层原理与面试题相关推荐

  1. HashMap底层原理分析(put、get方法)

    1.HashMap底层原理分析(put.get方法) HashMap底层是通过数组加链表的结构来实现的.HashMap通过计算key的hashCode来计算hash值,只要hashCode一样,那ha ...

  2. java map原理_Java HashMap底层原理分析

    前两天面试的时候,被面试官问到HashMap底层原理,之前只会用,底层实现完全没看过,这两天补了补功课,写篇文章记录一下,好记性不如烂笔头啊,毕竟这年头脑子它记不住东西了哈哈哈.好了,言归正传,今天我 ...

  3. HashMap底层原理(当你put,get时内部会发生什么呢?)

    HashMap底层原理解析(一) 接触过HashMap的小伙伴都会经常使用put和get这些方法,那接下来就对HashMap的内部存储进行详解.(以初学者的角度进行分析)-(小白篇) 当程序试图将多个 ...

  4. 深度解剖HashMap底层原理

    HashMap底层原理 写在前面 JDK1.7版本--HashMap java.1.7源码分析 new一个HashMap实例的存储流程图如下: API常用方法 API中重要的变量 第一步:申明一个Ha ...

  5. 我向面试官讲解了hashmap底层原理,他对我竖起了大拇指

    前言: 正值金九银十的黄金招聘期,大家都准备好了吗?HashMap是程序员面试必问的一个知识点,其内部的基本实现原理是每一位面试者都应该掌握的,只有真正地掌握了 HashMap的内部实现原理,面对面试 ...

  6. hashMap 底层原理+LinkedHashMap 底层原理+常见面试题

    1.源码 java1.7 hashMap 底层实现是数组+链表 java1.8 对上面进行优化 数组+链表+红黑树 2.hashmap 是怎么保存数据的. 在hashmap 中有这样一个结构 Node ...

  7. hashmap底层原理_周末自己动手撸一个 HashMap,美滋滋

    对HashMap的思考 通过写一个迷你版的HashMap来深刻理解 定义接口 接口实现 看MyHashMap的构造 Entry 看put如何实现 hash函数 resize和rehash get实现 ...

  8. 面试官再问你 HashMap 底层原理,就把这篇文章甩给他看

    来自:烟雨星空 前言 HashMap 源码和底层原理在现在面试中是必问的.因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合.文章较长,介绍了很多原理性的问题,希 ...

  9. Java集合—HashMap底层原理

    原文链接:最通俗易懂搞定HashMap的底层原理 HashMap的底层原理面试必考题.为什么面试官如此青睐这道题?HashMap里面涉及了很多的知识点,可以比较全面考察面试者的基本功,想要拿到一个好o ...

  10. 查询已有链表的hashmap_面试官再问你 HashMap 底层原理,就把这篇文章甩给他看...

    前言 HashMap 源码和底层原理在现在面试中是必问的.因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合.文章较长,介绍了很多原理性的问题,希望对你有所帮助~ ...

最新文章

  1. 规范化制度保障安擎高品质产品
  2. AI人工智能-智能学习时代
  3. python安装绘图库matplotlib_Python基础教程:Python 2D绘图库 Matplotlib 简介和安装
  4. Linux下gcc入门
  5. m_Orchestrate learning system---二十、如何写代码不容易犯错
  6. java.lang.OutOfMemoryError 解决方式
  7. CodeForces - 1562E Rescue Niwen!(dp)
  8. 工信部推动云计算健康快速发展 催生巨大市场机会
  9. 微信小程序入门三:轮播图
  10. Windows中命令提示符被禁用的解决方法
  11. Atitit oracle新特性5 6 7 8 9 10 11 12 18 19 20 attilax总结 目录 1.1. :ora 20c 1 1.2. Oracle Database 19c 的
  12. ios带嗅探器的浏览器_MAC系统下 有没有像WIN系统的傲游浏览器那样有嗅探功能的呢?...
  13. 谷歌5款插件推荐:谷歌工具类、翻译类扩展程序
  14. PS利用切片工具将一张大图裁剪成多个子图像并导出
  15. 苹果mac下载了dms文件怎么办?
  16. 电脑上如何禁止一切弹窗广告?永久关闭桌面弹出广告
  17. 详解美团外卖订单分配内部机制
  18. 盈鹏飞嵌入式_EVB-T335 TF卡功能测试手册
  19. kali win10双系统_win10安装kalilinux双系统详细步骤
  20. 抢占中国P2P海外第一股 宜人贷寓意为何

热门文章

  1. 如何搭建一个简单的QQ机器人(基于mirai)
  2. h5可以获取本机手机号码么_h5页面模板免费制作丨动态h5页面模板挑选
  3. 华为USG6000基本内容总结
  4. excel制作跨职能流程图_一款在线版流程图工具亿图图示
  5. 线程的三种创建方式以及区别
  6. 华南主板超频设置图解_支持XMP2.0,3000稳超3733MHz,威刚龙耀D60G超频体验
  7. 什么是自然语言处理技术
  8. 转载-极化码系列(2)-极化码的编码原理
  9. 如何把pdf转换成ezd_如何把pdf转换ppt格式
  10. 远程线程注入技术 屏蔽ctrl+alt+del