前言

最近阅读了许多HashMap实现及源码分析的文章,特意此文记录HashMap的知识点。
HashMap 底层由 数组 + 链表 组成,在 jdk1.7 和 1.8 中具体略有不同。

JDK1.7的HashMap

数据结构:图片来源

核心成员变量

图片来源

  1. 初始化桶大小(1<<4,即:16),因为底层是数组,所以这是数组默认的大小。
  2. 桶容量最大值。
  3. 默认的负载因子(0.75)
  4. table 真正存放数据的数组。
  5. Map 中存放元素数量。
  6. 桶的容量大小,可在初始化时显式指定。
  7. 负载因子,可在初始化时显式指定。

负载因子

存放的键值对数量(size) = 桶容量(threshold) * 负载因子(loadFactor)时,会发生扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。因此最好提前预估 HashMap 的大小,尽量的减少扩容带来的性能损耗。

Entry

Entry是HashMap的一个内部类,用于保存键值对,实现HashMap中的链表,主要成员变量:

  • key:写入的键。
  • value: 写入的值。
  • next:开始的时候就提到 HashMap 是由数组和链表组成,所以这个 next 就是用于实现链表结构。
  • hash: 存放的是当前 key 的 hashcode。

桶初始大小为16的原因

要解释这个问题,首先要知道这个容量的用途。容量就是一个HashMap中"桶"的个数(数组的大小),当想要往一个HashMap中put一个元素的时候,需要通过一定的算法计算出应该把他放到哪个桶中。HashMap中通过以下两个方法实现计算一个元素对应的桶(数组的索引)

  • int hash(Object k):该方法主要是将Object转换成一个整型。
  • int indexFor(int h, int length):该方法主要是将hash生成的整型转换成链表数组中的下标。jdk1.8没有此方法,不过计算的方式相同。
static int indexFor(int h, int length) {return h & (length-1);
}

在保证length(容量)是2^n 的前提下,h & (length-1)相当于h % (length-1),即用位运算(&)代替取模运算(%)

Java之所有使用位运算(&)来代替取模运算(%),最主要的考虑就是效率。
位运算(&)效率要比代替取模运算(%)高很多,主要原因是位运算直接对内存数据进行操作,不需要转成十进制,因此处理速度非常快。

为什么保证容量为2^n即使用位运算(&)来实现取模运算(%)

总结:因为位运算直接对内存数据进行操作,不需要转成十进制,所以位运算要比取模运算的效率更高,所以HashMap在计算元素要存放在数组中的index的时候,使用位运算代替了取模运算。而等价代替,前提是要求HashMap的容量一定要是2^n

由上述分析,容量只要为2^n即可,HashMap选择16的原因可能是个经验值。

既然一定要设置一个默认的2^n 作为初始值,那么就需要在效率和内存使用上做一个权衡。这个值既不能太小,也不能太大。太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。(官方未给出原因)

扩容

由上述分析:HashMap必须保证容量为2^n。因此在扩容时,HashMap会进行成倍的扩容(容量变为原来的2倍)。
扩容的步骤为:

  • 新建数组:创建一个新的Entry空数组,长度是原数组的2倍。
  • 重新计算hash:遍历原Entry数组,把所有的Entry重新Hash到新数组。

Put方法(头插法)

JDK1.7下的put方法添加新元素时使用头插法:即新来的值会成为头节点。

public V put(K key, V value) {//判断当前数组是否需要初始化if (table == EMPTY_TABLE) {inflateTable(threshold);}//如果 key 为空,则 put 一个空值进去。if (key == null)return putForNullKey(value);//计算键值hash值int hash = hash(key);//查找对应的桶的索引int i = indexFor(hash, table.length);//遍历链表for (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;//遍历判断里面的 hashcode、key 是否和传入 key 相等,//如果相等则进行覆盖,并返回原来的值。if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}}modCount++;//添加新键值对(头插法),会判断是否需要扩容addEntry(hash, key, value, i);return null;
}

头插法的问题

使用头插法,在扩容时会反转链表上元素的顺序。在多线程及需要扩容的条件下,可能出现环形链表,造成死循环。
jdk1.7HashMap出现环路(有个例子,但我感觉不是特别清楚)

JDK1.8的HashMap

JDK1.7的HashMap在 Hash 冲突严重时,桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。
因此JDK1.8重点解决的此问题。
数据结构:图片来源

主要区别

  • 新的成员变量 TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。链表长度大于等于该值时,会尝试转为红黑树(还需判断数组长度是否大于MIN_TREEIFY_CAPACITY
  • 新的成员变量 UNTREEIFY_THRESHOLD 用于判断是否需要红黑树转为链表的阈值。
  • 用Node代替Entry,在达到红黑树阈值时,将链表转为红黑树提高查询效率。
  • put方法添加新的元素时,由头插法改为尾插法。使用尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题。但在 HashMap 扩容的时候会调用 resize() 方法,此时并发操作仍然可能在一个桶上形成环形链表。
    JDK1.8下的HashMap依旧是线程不安全的,只是用尾插法代替头插法解决了JDK1.7时,容易出现环形链表的问题

转为红黑树的条件

默认情况下:链表长度大于 8(TREEIFY_THRESHOLD), 表的长度大于 64(MIN_TREEIFY_CAPACITY) 的时候会转化红黑树。

参考

  • HashMap? ConcurrentHashMap? 相信看完这篇没人能难住你!
  • 《吊打面试官》系列-HashMap
  • HashMap 为什么线程不安全?

一文理清HashMap的实现及细节相关推荐

  1. 一文理清散乱的物联网里开发者必须关注的技术!

    一文理清散乱的物联网里开发者必须关注的技术! 不管是从商业模式导出的业务模型,还是从技术发展的角度看,文本都倾向于将物联网技术构架看作是互联网技术构架的延展.而与这个观念对立的,是传统嵌入式软件开发的 ...

  2. MLK | 一文理清集成学习知识点(BoostingBagging)

    ???? 前情回顾 MLK | 那些常见的特征工程 MLK | 模型评估的一些事 MLK | 机器学习的降维"打击" MLK | 非监督学习最强攻略 MLK | 机器学习采样方法大 ...

  3. MLK | 一文理清深度学习循环神经网络

    MLK,即Machine Learning Knowledge,本专栏在于对机器学习的重点知识做一次梳理,便于日后温习,内容主要来自于<百面机器学习>一书,结合自己的经验与思考做的一些总结 ...

  4. 一文理清Mybatis中resultType与resultMap之间的关系和使用场景

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 1.概述 Mybatis ORM半自动映射框架对java开发工程师来说应该是必会的框架之一. ...

  5. 一文理清面向对象(封装、继承、多态)+ 实战案例

    python是一门面向对象编程语言,对面向对象语言编码的过程叫做面向对象编程. 面向对象是一种思想,与之相对的是面向过程.我们先简单说一下面向过程. 面向过程其实就是把过程当做设计核心,根据问题的发展 ...

  6. 一文理清Cookie、Session、Token

    发展史 1.很久很久以前,Web 基本上就是文档的浏览而已, 既然是浏览,作为服务器, 不需要记录谁在某一段时间里都浏览了什么文档,每次请求都是一个新的HTTP协议, 就是请求加响应, 尤其是我不用记 ...

  7. 一文理清区块链里那些容易混淆的概念

    本文为智链ChainNova投稿文章. 区块链技术分享.行业分析等文章投稿可邮件至 jiawd@csdn.net或微信联系 jiaweidi1214 我们在研究区块链的过程中发现,区块链的发展和云计算 ...

  8. 一文理清数据仓库实施方法论

    关键的原则包括: 以数据仓库技术为核心平台 数据平台的设计必须解决现有问题,同时着眼于未来 促进一致性和跨部门的整合 剔除重复的数据 保留事件的历史和事件的相关内容 收集和保存最原始的数据 收集满足现 ...

  9. 一文理清H5调起App那些事

    以安卓为例,实现h5调起app步骤: 在安卓AndroidManifest.xml中,启动Activity下添加属性: <intent-filter><action android: ...

最新文章

  1. 怎么样才能快速的把淘宝店铺推广出去
  2. 百度大脑发挥AI“头雁效应” 王海峰:在AI时代共同推动社会智能化升级
  3. 全球人工智能战略与政策观察(2019)
  4. POS 收款机资料整理
  5. python下载大文件-python-Django:允许用户下载大文件
  6. Android Activity动画属性简介
  7. h桥控制电机刹车_082 电机驱动桥集成式的结构,定速比10左右,松油门或轻踩刹车瞬间,出现齿轮撞击的情况,是什么原因导致的?应该采取什么措施?...
  8. python运行结果闪退_Pyhton TestCase运行闪退与失败,原因不详。。。
  9. oracle审计实施
  10. CGContextRef CIImageRef详解
  11. 码农你会搜“Win10 破解版”吗?
  12. linux tar 命令使用
  13. linux服务sendmail邮件服务
  14. Default process group has not been initialized, please make sure to call init_process_group
  15. [Swift]LeetCode1023. 驼峰式匹配 | Camelcase Matching
  16. 通过names.index()方法找到第2个eva值 ,并将其改成EVA
  17. MATLAB简介与基础知识
  18. JavaScript(十二)常见js特效
  19. 那些年,我们信了课本里的那些鬼话
  20. 计算机网络回顾之计算机网络概述

热门文章

  1. Matlab与高等数学
  2. P3389 【模板】高斯消元法
  3. Shift and Reverse
  4. 疾病预测和天气分析练习赛
  5. YBTOJ:彩球抽取(期望)
  6. P3694-邦邦的大合唱站队【状压dp】
  7. P4564-[CTSC2018]假面【期望dp】
  8. jzoj1751-Span(每日C组)【并查集,贪心】
  9. 公共子串 字符串哈希
  10. Spark入门(五)Spark SQL shell启动方式(元数据存储在derby)