Java面试知识点(六)hashmap深度理解
1、hashmap 的数据结构
要知道 hashmap 是什么,首先要搞清楚它的数据结构,在 java 编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,hashmap 也不例外。Hashmap 实际上是一个数组和链表的结合体(在数据结构中,一般称之为 “链表散列 “),请看下图(横排表示数组,纵排表示数组元素【实际上是一个链表】)。
注:关于hashcode和equals方法的解释,可以看
Java面试知识点(一)hashmap、hashtable和hashset
第5个知识点
从图中我们可以看到一个 hashmap 就是一个数组结构,当新建一个 hashmap 的时候,就会初始化一个数组。我们来看看 java 代码:
/**
* The table, resized as necessary. Length MUST Always be a power of two.
* FIXME 这里需要注意这句话,至于原因后面会讲到
*/
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
final int hash;
Entry<K,V> next;
..........
}
上面的 Entry 就是数组中的元素,它持有一个指向下一个元素的引用,这就构成了链表。
当我们往 hashmap 中 put 元素的时候,先根据 key 的 hash 值得到这个元素在数组中的位置(即下标),然后就可以把这个元素放到对应的位置中了。如果这个元素所在的位子上已经存放有其他元素了,那么在同一个位子上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。从 hashmap 中 get 元素时,首先计算 key 的 hashcode,找到数组中对应位置的某一元素,然后通过 key 的 equals 方法在对应位置的链表中找到需要的元素。从这里我们可以想象得到,如果每个位置上的链表只有一个元素,那么 hashmap 的 get 效率将是最高的,但是理想总是美好的,现实总是有困难需要我们去克服,哈哈~
2、hash 算法
我们可以看到在 hashmap 中要找到某个元素,需要根据 key 的 hash 值来求得对应数组中的位置。如何计算这个位置就是 hash 算法。前面说过 hashmap 的数据结构是数组和链表的结合,所以我们当然希望这个 hashmap 里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用 hash 算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。
所以我们首先想到的就是把 hashcode 对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模” 运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那?java 中时这样做的,
static int indexFor(int h, int length) {
return h & (length-1);
}
首先算得 key 得 hashcode 值,然后跟数组的长度 - 1 做一次 “与” 运算(&)。看上去很简单,其实比较有玄机。比如数组的长度是 2 的 4 次方,那么 hashcode 就会和 2 的 4 次方 - 1 做 “与” 运算。很多人都有这个疑问,为什么 hashmap 的数组初始化大小都是 2 的次方大小时,hashmap 的效率最高,我以 2 的 4 次方举例,来解释一下为什么数组大小为 2 的幂时 hashmap 访问的性能最高。
看下图,左边两组是数组长度为 16(2 的 4 次方),右边两组是数组长度为 15。两组的 hashcode 均为 8 和 9,但是很明显,当它们和 1110 “与” 的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8 和 9 会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到 8 或者 9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为 15 的时候,hashcode 的值会与 14(1110)进行 “与”,那么最后一位永远是 0,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!
所以说,当数组长度为 2 的 n 次幂的时候,不同的 key 算得得 index 相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。
说到这里,我们再回头看一下 hashmap 中默认的数组大小是多少,查看源代码可以得知是 16,为什么是 16,而不是 15,也不是 20 呢,看到上面 annegu 的解释之后我们就清楚了吧,显然是因为 16 是 2 的整数次幂的原因,16-1的二进制是 1 1 1 1 ,能够尽量报错hash值的结果,在小数据量的情况下 16 比 15 和 20 更能减少 key 之间的碰撞,而加快查询的效率。
所以,在存储大容量数据的时候,最好预先指定 hashmap 的 size 为 2 的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的 2 次幂来初始化的,代码如下 (HashMap 的构造方法中):
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
3、hashmap 的 resize
当 hashmap 中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对 hashmap 的数组进行扩容,数组扩容这个操作也会出现在 ArrayList 中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的 “均摊” 原理,就释然了,而在 hashmap 数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是 resize。
那么 hashmap 什么时候进行扩容呢?当 hashmap 中的元素个数超过数组大小 * loadFactor 时,就会进行数组扩容,loadFactor 的默认值为 0.75,也就是说,默认情况下,数组大小为 16,那么当 hashmap 中元素个数超过 16*0.75=12 的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知 hashmap 中元素的个数,那么预设元素的个数能够有效的提高 hashmap 的性能。比如说,我们有 1000 个元素 new HashMap (1000), 但是理论上来讲 new HashMap (1024) 更合适,不过上面 annegu 已经说过,即使是 1000,hashmap 也自动会将其设置为 1024。 但是 new HashMap (1024) 还不是更合适的,因为 0.75*1000 < 1000, 也就是说为了让 0.75 * size > 1000, 我们必须这样 new HashMap (2048) 才最合适,既考虑了 & 的问题,也避免了 resize 的问题。
- 如果需要扩容,调用扩容的方法 resize ()
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
//判断是否有超出扩容的最大值,如果达到最大值则不进行扩容操作
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
// transfer()方法把原数组中的值放到新数组中
transfer(newTable, initHashSeedAsNeeded(newCapacity));
//设置hashmap扩容后为新的数组引用
table = newTable;
//设置hashmap扩容新的阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
- transfer () 在实际扩容时候把原来数组中的元素放入新的数组中
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//通过key值的hash值和新数组的大小算出在当前数组中的存放位置
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
4、key 的 hashcode 与 equals 方法改写
在第一部分 hashmap 的数据结构中,就写了 get 方法的过程:首先计算 key 的 hashcode,找到数组中对应位置的某一元素,然后通过 key 的 equals 方法在对应位置的链表中找到需要的元素。所以,hashcode 与 equals 方法对于找到对应元素是两个关键方法。
Hashmap 的 key 可以是任何类型的对象,例如 User 这种对象,为了保证两个具有相同属性的 user 的 hashcode 相同,我们就需要改写 hashcode 方法,比方把 hashcode 值的计算与 User 对象的 id 关联起来,那么只要 user 对象拥有相同 id,那么他们的 hashcode 也能保持一致了,这样就可以找到在 hashmap 数组中的位置了。如果这个位置上有多个元素,还需要用 key 的 equals 方法在对应位置的链表中找到需要的元素,所以只改写了 hashcode 方法是不够的,equals 方法也是需要改写滴~当然啦,按正常思维逻辑,equals 方法一般都会根据实际的业务内容来定义,例如根据 user 对象的 id 来判断两个 user 是否相等。
在改写 equals 方法的时候,需要满足以下三点:
(1) 自反性:就是说 a.equals (a) 必须为 true。
(2) 对称性:就是说 a.equals (b)=true 的话,b.equals (a) 也必须为 true。
(3) 传递性:就是说 a.equals (b)=true,并且 b.equals (\c)=true 的话,a.equals (\c) 也必须为 true。
通过改写 key 对象的 equals 和 hashcode 方法,我们可以将任意的业务对象作为 map 的 key (前提是你确实有这样的需要)。
总结:
本文主要描述了 HashMap 的结构,和 hashmap 中 hash 函数的实现,以及该实现的特性,同时描述了 hashmap 中 resize 带来性能消耗的根本原因,以及将普通的域模型对象作为 key 的基本要求。尤其是 hash 函数的实现,可以说是整个 HashMap 的精髓所在,只有真正理解了这个 hash 函数,才可以说对 HashMap 有了一定的理解。
Java面试知识点(六)hashmap深度理解的更多相关文章
- Java 面试知识点解析(六)——数据库篇
前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...
- Java面试知识点汇总
Java面试知识点汇总 置顶 2019年05月07日 15:36:18 温柔的谢世杰 阅读数 21623 文章标签: 面经java 更多 分类专栏: java 面试 Java面试知识汇总 版权声明 ...
- Java 面试知识点解析(二)——高并发编程篇
前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...
- Java 面试知识点解析(四)——版本特性篇
前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...
- Java 面试知识点解析(五)——网络协议篇
前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...
- Java 面试知识点解析(七)——Web篇
前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...
- Java 面试知识点解析(三)——JVM篇
前言: 在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Java 知识点进行复习和学习一番,大 ...
- Java面试知识点之线程篇(二)
前言:接上篇,这里继续对java线程相关知识点进行总结. 1.notify和notifyall的区别 notify()方法能够唤醒一个正在等待该对象的monitor的线程,当有多个线程都在等待该对象的 ...
- Java面试知识点之线程篇(三)
前言:这里继续对java线程相关知识点进行总结,不能间断. 1.yield()方法 yield()的作用是让步.它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执 ...
- Java面试知识点之线程篇(一)
前言:在Java面试中,一定会遇到线程相关问题,因此笔者在这里总结Java中有关线程方面知识点,多数从网上得来(文中会贴出主要参考链接),有些也是笔者在面试中所遇到的问题,如有错误,请不吝指正.主要参 ...
随机推荐
- python教程3.1:数据类型:字符串+列表list
一.字符串 字符串是⼀个有序的字符的集合,⽤于在计算机⾥存储和表示⽂本信息 常用方法--str.fun() 全局函数 二.列表list []内以逗号分隔,按照索引,存放各种数据类型,每个位置代表⼀个 ...
- 前端scale负数表示翻转
https://blog.csdn.net/wang_yu_shun/article/details/121299208 极力推荐这个博主写的,前端有关负数的小技巧
- 移动端termux安装kali
1.相关准备一部安卓手机,termux,NVAC,浏览器2.安装kali首先进入kali的官网选择文档找到Android手机上的kali找到NetHunter-Rootless找到kali安装命令:t ...
- C语言:单链表删除学生信息,增加学生信息(简易版)
假设用户都是正常的,不会输入一些乱七八糟的东西. 功能1:输出学生学号和成绩,用动态连链表来存放,继续存放学生信息的时候可以继续输入之前输入过的学号信息,打印的时候会分开打印(因为是简易版本,没有太完 ...
- Java 中的深拷贝和浅拷贝你了解吗?
前言 Java 开发中,对象拷贝是常有的事,很多人可能搞不清到底是拷贝了引用还是拷贝了对象.本文将详细介绍相关知识,让你充分理解 Java 拷贝. 一.对象是如何存储的? 方法执行过程中,方法体中的数 ...
- 阿里巴巴 MySQL 数据库之建表规约(一)
建表规约 强制部分 [强制] 表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint (1 表示是,0 表示否). 说明:任何字段如果为非负数,必须是 ...
- CSS——鼠标样式
在浏览网页的过程中,当我们将鼠标移动到一些元素上时,鼠标的样式会发生相应的改变,例如当鼠标指向文本时,鼠标的样式会变成类似大写字母I的样子:当鼠标指向链接时,鼠标会变成一个小手的形状等. 除了这些默认 ...
- Android 13 - Media框架(12)- MediaCodec(二)
关注公众号免费阅读全文,进入音视频开发技术分享群! 前面一节我们学习了 MediaCodec 的创建以及配置过程,了解部分设计机制以及功能,这一节我们将继续学习其他方法. 1.start start ...
- 一文教你如何调用Ascend C算子
本文分享自华为云社区<一文教你如何调用Ascend C算子>,作者: 昇腾CANN. Ascend C是CANN针对算子开发场景推出的编程语言,原生支持C和C++标准规范,兼具开发效率和运 ...
- CF1836
A.Destroyer 开个桶记录个数,看满不满足单调不上升即可. B.Astrophysicists 辛辛苦苦写了这么久的文章就没了????烦死了. 自己做 Virtual Contest 的时候这 ...