//本文章讲述关于HashMap的定义和主要方法,底层数据结构,调用put方法时的底层操作,哈希碰撞的三种情况以及解决办法以及hashmap的扩容机制
一 Map的定义和主要方法
1 Map的定义:
Map是表示键值对映射关系的接口,输入为键值对。
1.1Map的核心特性
键唯一而且不可以重复,每一个键对应一个值,值可以重复(在输入时其中键或者键的哈希码相同的情况称为哈希冲突,在后文解释)
1.2.Map的常用方法及说明:
方法签名 | 功能说明 |
---|---|
Void put(K key, V value) | 添加键值对,返回被替换的旧值(无则返回null) |
Void get(Object key) | 根据键获取对应值 |
boolean containsKey(Object key) | 判断是否包含指定键 |
boolean containsValue(Object v) | 判断是否包含指定值 |
Set<K> keySet() | 返回所有键组成的Set集合 |
Collection<V> values() | 返回所有值组成的Collection集合 |
Set<Map.Entry<K,V>> entrySet() | 返回键值对Entry构成的Set集合 |
Void remove(Object key) | 删除指定键对应的键值对 |
default V getOrDefault(...) | (JDK8+) 安全获取值,不存在时返回默认值 |
int size() | 返回键值对数量 |
典型实现类:
- HashMap:基于哈希表的无序实现(允许null键/值)
- TreeMap:基于红黑树的有序映射(按键的自然顺序排序)
- LinkedHashMap:保持插入顺序的哈希映射
注意:使用entrySet()
遍历效率最高,推荐写法:
for(Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
}
二 HashMap
2.1.HashMap的底层数据结构
HashMap的底层数据结构是数组,链表,红黑树,结合了数组,链表,红黑树的优点,查询,增删改查的效率都比较高
2.2.HashMap中的加载因子
哈希表中的加载因子(load factor)是衡量哈希表空间利用效率的重要参数,定义为:
加载因子 = 已存储元素数量 \ 哈希表总容量
核心作用
-
冲突概率评估
值越接近1时,哈希冲突概率显著增大。例如当加载因子达到0.8时,发生冲突的可能性比0.5时高约60%(具体数值与哈希函数质量相关) -
扩容触发机制
多数实现设定阈值(如Java HashMap默认0.75)。当超过阈值时触发扩容:- 新建2倍容量数组(常见策略)
- 重哈希所有现存元素
- 该操作时间复杂度为 O(n)
-
性能平衡杠杆
- 高负载因子(如0.9):节省30%内存,但查询时间可能增加50%
- 低负载因子(如0.5):提升20%查询速度,但内存消耗翻倍
三.HashMap的构造方法
3.1无参构造
HashMap的无参构造默认的底层数组容量是16,加载因子是0.75F
实际上,调用无参构造只是把0.75赋值给加载因子,并没有立即创建长度为16的数组,而是在第一次调用HashMap中的put();方法时创建了一个容量为16 的数组。
HashMap();使用默认初始容量16和默认加载因子0.75创建一个的HashMap。
3.2有参构造
3.2.1仅指定初始容量的有参构造
按照指定的容量来创建数组,加载因子依然是0.75
实际上初始化容量会按照HashMap的底层运算来创建,最终会变成2的n次方的形式
HashMap(int initialCapacity)使用指定的初始容量来创建一个空HashMap
3.2.2指定初始容量和加载因子
HashMap(int initialCapacity,float LoadFactor)使用指定的初始容量来创建一个空HashMap
四 HashMap首次调用put方法时的底层步骤
1.初始化存储数组
由于HashMap在调用构造方法时并不会创建存储数组,而是在首次调用put方法时创建,所以调用put方法的第一件事就是按照默认长度或指定长度创建并初始化存储数组
检查哈希表table是否为空,如果未初始化,则通过resize()方法创建指定长度或默认长度16的数组。
2.计算哈希值
通过hashcode()计算键的哈希值,尽量减少哈希冲突。
3.确定桶索引
通过哈希值与数组长度取模运算计算存储位置
4.插入新节点
若目标桶为空,则直接创建新节点Node<K,V>
并存入该位置。
5.更新状态
五 哈希碰撞(冲突)的三种情况
5.1情况一 key的值一样,hashcode自然一样
在这种情况下新的value会替代旧的value,,并且会返回旧值。
Map<Integer,String> hashmap=new HashMap<>();
hashmap.put(1,"ashiahd");
String m=hashmap.put(1,"ashi");
System.out.println(m);
System.out.println(hashmap);
//输出结果
//ashiahd
//{1=ashi}
5.2情况二key的值不一样,但是经过哈希运算得出的hashcode是一样的
hashmap.put("NB",1);
System.out.println("NB".hashCode());
hashmap.put("Ma",2);
System.out.println("Ma".hashCode());
/**
2484
2484
{NB=1, Ma=2}
*/
5.3情况三 key和hashcode都不一样,但是在最后与数组长度取模运算后结果一致,导致存储在数组的相同位置
//情况三,key值hashcode都不同,但是模运算后储存在数组相同位置
hashmap.put("a",1);
hashmap.put("b",2);
hashmap.put("c",3);
hashmap.put("d",4);
hashmap.put("e",5);
hashmap.put("f",6);
System.out.println(hashmap);
六 哈希碰撞的解决办法
6.1链地址法(Chaining)
链地址的思想为将相同的哈希值的元素根据输入的先后顺序以链表的形式进行储存,但是当链表长度大于8时一般会转为红黑树进行储存。
这种方法通过将所有具有相同哈希值的对象链接到同一个位置。当哈希表的某个桶已经有元素时,新插入的元素会形成一个链表。查询时,如果找到对应的链表,则遍历链表直到找到目标对象。
6.2开放定址法(Open Addressing)
这种方法通过寻找下一个可用的位置来解决碰撞。常见的处理方法包括:
- 线性探测:逐个检查下一个位置,直到找到空位。
- 二次探测:使用不同的步长或平方函数来找下一个位置。
- 双哈希探测:在碰撞时使用另一个哈希函数计算新位置。
Java的HashMap
从JDK 8开始引入了动态数组和链表结构,并且当链表长度超过树化阈值(默认为8)时,会转为使用开放定址法中的二次探测来插入元素。
6.3调整负载因子(Load Factor)
设置合适的负载因子可以控制哈希表的容量增长,减少碰撞的可能性。通常默认负载因子为0.75,表示当表中元素数量达到容量的75%时会触发重哈希。
6.4 自定义哈希函数
通过实现自定义的hashCode()
和equals()
方法,可以减少不同对象之间发生哈希碰撞的可能性。
七 HashMap的扩容机制
7.1 HashMap的扩容机制是为了保持较低的负载因子,避免哈希表性能下降。
当实际存储的元素数量超过了容量乘以预设的负载因子(默认为0.75)时,HashMap会触发扩容操作,将容量增加一倍(通常是2的幂次方)。这样可以减少哈希冲突,提高查找和插入操作的效率。每次扩容时,所有键值对都会重新分布到新的更大的数组中,确保性能得到优化。
7.2 为什么HashMap在进行扩容时选择2的幂次作为新的容量
高效的索引计算:在HashMap中,键的哈希值通过与当前容量取模运算(hash % capacity
)来确定存储位置。当容量是2的幂次时,这个取模运算可以优化为更快速的位操作,即使用位掩码(hash & (capacity - 1)
)。这种位操作比传统的模运算快得多
减少哈希冲突:使用2的幂次作为容量有助于提高哈希函数的表现。由于容量是2的幂次,哈希值在分配到数组中的位置时更均匀,从而减少了碰撞的概率。
方便内存管理:计算机内存通常以2的幂次进行划分和管理。使用2的幂次作为容量可以更好地利用内存资源,减少碎片化,并提高内存访问效率。
高效的扩容策略:每次将容量翻倍(即乘以2),确保新容量仍然是2的幂次。这种做法简化了扩容逻辑,避免了处理非2的幂次带来的复杂性。
初始容量自动调整:当用户指定的初始容量不是2的幂次时,HashMap会将其自动调整到大于等于该值的最小2的幂次。例如,如果指定容量为10,则实际分配的容量为16(即2^4
)。