- 参考博客
Java中的equals和hashCode方法详解
链表
java提高篇(二三)—–HashMap Java
HashMap的工作原理
算法的时间复杂度和空间复杂度-总结
Hashmap
Hash (散列函数)
第1部分 HashMap介绍
HashCode的作用原理和实例解析
hash碰撞处理
高性能场景下,HashMap的优化使用建议
- 整体思路
1. 什么是HashMap?有什么作用?
2. 了解HashMap原理前,需要知道哪些知识?
3. 分析HashMap原理。
4. 应用场景。
- 什么是HashMap?有什么作用?
什么是HashMap: 基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(get 和 put)提供稳定的性能。迭代 collection 视图所需的时间与 HashMap 实例的“容量”(桶的数量)及其大小(键-值映射关系数)成比例。所以,如果迭代性能很重要,则不要将初始容量设置得太高(或将加载因子设置得太低)。
作用:通过哈希算法,通过键值可快速找到自己需要的对象。
- 了解HashMap原理前,需要知道哪些知识??
-
集合
-
HashMap相关接口
-
哈希算法
-
散列(哈希)表
-
链表
-
散列碰撞
-
算法复杂度计算
-
集合:集合类存放的都是对象的引用,而非对象本身,出于表达上的便利,我们称集合中的对象就是指集合中对象的引用(reference)。集合类型主要有3种:set(集)、list(列表)和map(映射)。
-
Map相关接口:HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
-
HashMap的构造函数:
HashMap() // 默认构造函数
HashMap(int capacity) // 指定“容量大小”的构造函数
HashMap(Map ? extends K, ? extends V> map) // 包含“子Map”的构造函数
HashMap(int capacity, float loadFactor) // 指定“容量大小”和“加载因子”的构造函数
Entry: 存储数据的Entry数组,长度是2的幂。HashMap是采用拉链法实现的,每一个Entry本质上是一个单向链表 ,如图可以很好的解释哈希结构:

每次新建一个HashMap时,都会初始化一个table数组。table数组的元素为Entry节点 -
哈希算法
哈希算法:哈希算法将任意长度的二进制值映射为较短的固定长度的二进制值,这个小的二进制值称为哈希值。哈希值是一段数据唯一且极其紧凑的数值表示形式。如果散列一段明文而且哪怕只更改该段落的一个字母,随后的哈希都将产生不同的值。要找到散列为同一个值的两个不同的输入,在计算上是不可能的,所以数据的哈希值可以检验数据的完整性。一般用于快速查找和加密算法。[参考知乎第一条答案](https://www.zhihu.com/question/20820286),比如这里有一万首歌,给你一首新的歌X,要求你确认这首歌是否在那一万首歌之内。无疑,将一万首歌一个一个比对非常慢。但如果存在一种方式,能将一万首歌的每首数据浓缩到一个数字(称为哈希码)中,于是得到一万个数字,那么用同样的算法计算新的歌X的编码,看看歌X的编码是否在之前那一万个数字中,就能知道歌X是否在那一万首歌中。
hasCode方法:从Object角度看,JVM每new一个Object,它都会将这个Object丢到一个Hash表中去,这样的话,下次做Object的比较或者取这个对象的时候(读取过程),它会根据对象的HashCode再从Hash表中取这个对象。这样做的目的是提高取对象的效率。若HashCode相同再去调用equal。
hash方法:
static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }indexFor方法(落桶):对于HashMap的table而言,数据分布需要均匀(最好每项都只有一个元素,这样就可以直接找到),不能太紧也不能太松,太紧会导致查询速度慢,太松则浪费空间。计算hash值后,怎么才能保证table元素分布均与呢?我们会想到取模,但是由于取模的消耗较大,HashMap是这样处理的:调用indexFor方法。
static int indexFor(int h, int length) {
return h & (length-1);
}
table数组长度:hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。 -
哈希(散列)表
哈希表:散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
哈希函数:(链地址法hashmap使用)采用数组和链表相结合的办法,将Hash地址相同的记录存储在一张线性表中,而每张表的表头的序号即为计算得到的Hash地址

-
链表
结构:每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取 -
散列碰撞
对象Hash的前提是实现equals()和hashCode()两个方法,那么HashCode()的作用就是保证对象返回唯一hash值,但当两个对象计算值一样时,这就发生了碰撞冲突。
冲突解决:前提是hash一致性的情况。hash函数相当于,把原空间的一个数据集映射到另外一个空间。四种方式可以查看这个。
满足三个原则
增大 映射空间/原空间 的大小
尽可能把原数据集均匀映射到较小空间
结合原空间数据的数据特征 -
原理分析
- 创建一个测试类,创建一个hashmap随机插入一个数据,打入断点查看hashmap的结构

1.有一个table数组,长度为16,随机存放之前put进去的key,value对象,这个对象的名称叫Entry
2.Entry包含了四个对象,hash,key,next,value
创建一个对象EntryTest,重写他的hashCode方法,直接写死让所有的返回值都是1

随机插入几个数据,查看hashmap结构
这个时候,虽然插入3个对象table的数据长度为1,点开Entry中的next属性发现每个next都存放着一个Entry对象,这便是链表形式的存储数据,hashmap采用的是哈希函数中拉链法存储数据,数组+链表。查看源码中的addEntry方法:
void addEntry(int hash, K key, V value, int bucketIndex) {
//获取bucketIndex处的Entry
Entry<K, V> e = table[bucketIndex];
//将新创建的 Entry 放入 bucketIndex 索引处,并让新的 Entry 指向原来的 Entry
table[bucketIndex] = new Entry<K, V>(hash, key, value, e);
//若HashMap中元素的个数超过极限了,则容量扩大两倍
if (size++ >= threshold)
resize(2 * table.length);
}
了解基本的hashmap结构,我们主要从拉链法来分析hashmap的put和get方法。查看put源码:
/**
* Associates the specified value with the specified key in this map. If the
* map previously contained a mapping for the key, the old value is
* replaced.
*
* @param key
* key with which the specified value is to be associated
* @param value
* value to be associated with the specified key
* @return the previous value associated with <tt>key</tt>, or <tt>null</tt>
* if there was no mapping for <tt>key</tt>. (A <tt>null</tt> return
* can also indicate that the map previously associated
* <tt>null</tt> with <tt>key</tt>.)
*/
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<k , V> e = table[i]; e != null; e = e.next) {
Object k;
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;
}
1.对key做null的检查,如果为null,会被存储在table[0]中,因为null的hash值总是为0
2.计算好hash值,hash值即为存在table数组中的索引,为了考虑到table数组的均匀分配,最好能保证每个里面都能分配到一个,增加查询速度
3.indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引,HashMap的底层数组长度总是2的n次方,在构造函数中存在:capacity <<= 1;这样做总是能够保证HashMap的底层数组长度为2的n次方。当length为2的n次方时,h&(length - 1)就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化。至于为什么是2的n次方点击这篇博客。
4.通过hash算法可以知道,如果2个key有相同的hash值,此为hash碰撞,相同的hash值会以链表的形式存储:
如果在刚才计算出来的索引位置没有元素,直接把Entry对象放在那个索引上。
如果索引上有元素,然后会进行迭代,一直到Entry->next是null。当前的Entry对象变成链表的下一个节点。
如果我们再次放入同样的key会怎样呢?逻辑上,它应该替换老的value。事实上,它确实是这么做的。在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)),如果这个方法返回true,它就会用当前Entry的value来替换之前的value。
这里要注意两个问题:
一是链的产生。这是一个非常优雅的设计。系统总是将新的Entry对象添加到bucketIndex处。如果bucketIndex处已经有了对象,那么新添加的Entry对象将指向原有的Entry对象,形成一条Entry链,但是若bucketIndex处没有Entry对象,也就是e==null,那么新添加的Entry对象指向null,也就不会产生Entry链了。
二、扩容问题。 随着HashMap中元素的数量越来越多,发生碰撞的概率就越来越大,所产生的链表长度就会越来越长,这样势必会影响HashMap的速度,为了保证HashMap的效率,系统必须要在某个临界点进行扩容处理。该临界点在当HashMap中元素的数量等于table数组长度加载因子,默认是0.75也就是说160.75=13就开始扩容了。但是扩容是一个非常耗时的过程,因为它需要重新计算这些数据在新table数组中的位置并进行复制处理。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
- bucketIndex相当于缓冲池的作用,避免了每次产生Entry对象是都去操作table数组,对象在产生的时候只需要去关心bucketIndex位置的元素就可以完成整个数组元素的插入和创建
- 为了防止链表的长度过长,系统会设置临界点进行扩容,临界点为table数组长度*加载因子。但是扩容是一个非常耗时的过程如果我们预知hashmap中的元素进行设计就能有效提高hashmap性能
put方法的实现还是围绕数组加链表的形式,需要注意的是碰撞和扩容的问题。接下来是存储的问题,查看put方法的实现:
public V get(Object key) {
// 若为null,调用getForNullKey方法返回相对应的value
if (key == null)
return getForNullKey();
// 根据该 key 的 hashCode 值计算它的 hash 码
int hash = hash(key.hashCode());
// 取出 table 数组中指定索引处的值
for (Entry<K, V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
//若搜索的key与查找的key相同,则返回相对应的value
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
围绕数组加链表结构,通过key的hash值找到table数组中的Entry ,key是以String的形式存储在Entry对象中的,只需要使用equals方法匹配到对应链表中的key取到value即可获取到value的值:

- 应用场景
- 如何优化
1.考虑加载因子地设定初始大小
2.减小加载因子
3.String类型的key,不能用==判断或者可能有哈希冲突时,尽量减少长度
4.使用定制版的EnumMap
5.使用IntObjectHashMap
优化从提高查询效率和减少系统开销分析。
加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。
对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;
如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。
本文深入探讨了Java中HashMap的内部工作原理,包括其基于哈希表的实现方式、哈希算法的应用、散列碰撞处理策略及优化技巧。适用于希望深入了解HashMap内部机制的开发者。

1023

被折叠的 条评论
为什么被折叠?



