映射(HashMap)

//本文章讲述关于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. 冲突概率评估
    值越接近1时,哈希冲突概率显著增大。例如当加载因子达到0.8时,发生冲突的可能性比0.5时高约60%(具体数值与哈希函数质量相关)

  2. 扩容触发机制
    多数实现设定阈值(如Java HashMap默认0.75)。当超过阈值时触发扩容:

    • 新建2倍容量数组(常见策略)
    • 重哈希所有现存元素
    • 该操作时间复杂度为 O(n)
  3. 性能平衡杠杆

    • 高负载因子(如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)。

<think>好的,我现在需要帮助用户了解如何在Java中使用HashMap实现键值对映射。根据用户的问题,他们可能对HashMap的基本用法、实现原理以及相关注意事项感兴趣。首先,我应该整理用户提供的引用内容,看看有哪些信息可以直接利用。 用户提供了四个引用,其中引用[1]提到了HashMap的基本操作,包括创建实例、添加、获取、检查、删除和遍历键值对。引用[2]和[4]讨论了如何排序HashMap,而引用[3]强调了键对象必须正确实现hashCode()和equals()方法。这些信息都是回答用户问题的重要部分。 首先,我需要从基础开始,逐步引导用户了解HashMap的使用。应该先介绍如何创建HashMap实例,然后依次讲解添加、获取、检查存在性、删除和遍历操作。这部分内容在引用[1]中有示例代码,可以直接参考。例如,创建实例的部分需要导入java.util.HashMap,并初始化泛型类型。 接下来,键对象的hashCode()和equals()方法的正确实现是关键,这一点在引用[3]中提到。需要解释为什么这两个方法很重要,以及如何正确覆盖它们,尤其是在使用自定义对象作为键时。比如,如果两个对象相等,它们的hashCode必须相同,否则会导致HashMap无法正确找到对应的值。 关于排序,用户可能想知道虽然HashMap本身无序,但可以通过转换为List并排序来实现,如引用[2]和[4]所述。这部分可以作为进阶内容,说明如何按键或值排序,可能需要使用Comparator和Collections.sort方法。 在实现方式方面,用户可能对HashMap的底层数据结构感兴趣。需要解释HashMap是基于哈希表实现的,使用数组和链表(或红黑树,在Java 8之后)来处理冲突。这涉及负载因子、扩容机制等概念,这些内容在引用[2]的源码分析部分有所提及。 还要注意示例代码的正确性,确保语法无误,比如泛型的使用和方法的正确调用。例如,在遍历键值对时,使用entrySet()获取所有条目,然后遍历每个Entry对象来获取键和值。 最后,生成相关问题时要涵盖用户可能进一步探索的方向,比如性能优化、线程安全、处理冲突的方法以及与其他集合类的比较。这些问题可以帮助用户深入理解HashMap的应用场景和限制。 总结一下,回答的结构应该包括基本用法、实现原理、注意事项和进阶操作,每个部分结合引用内容,确保信息准确。同时,示例代码要清晰,注释详细,帮助用户快速上手。</think>### 使用HashMap实现键值对映射 #### 1. **HashMap的基本用法** HashMapJava中基于哈希表的键值对存储结构,支持高效插入、删除和查找操作。以下是核心实现步骤: **1.1 创建HashMap实例** ```java import java.util.HashMap; public class Main { public static void main(String[] args) { // 声明键为String类型,值为Integer类型的HashMap HashMap<String, Integer> map = new HashMap<>(); } } ``` 键值类型需通过泛型指定,支持任意对象类型[^1]。 **1.2 添加键值对** ```java map.put("apple", 10); // 添加键"apple"对应值10 map.put("banana", 5); // 添加键"banana"对应值5 ``` 若键已存在,新值会覆盖旧值[^1]。 **1.3 获取值** ```java int count = map.get("apple"); // 返回10 int missing = map.getOrDefault("grape", 0); // 键不存在时返回默认值0 ``` **1.4 检查键/值存在性** ```java boolean hasKey = map.containsKey("apple"); // true boolean hasValue = map.containsValue(5); // true ``` **1.5 删除键值对** ```java map.remove("banana"); // 删除键为"banana"的条目 ``` **1.6 遍历操作** ```java // 遍历所有键值对 for (HashMap.Entry<String, Integer> entry : map.entrySet()) { System.out.println(entry.getKey() + ": " + entry.getValue()); } ``` --- #### 2. **实现原理** - **数据结构**:基于数组+链表(Java 8后链表长度>8时转为红黑树)[^2]。 - **哈希函数**:键的`hashCode()`计算哈希值,决定存储位置。 - **冲突解决**:链表法处理哈希冲突,Java 8优化为红黑树提升查询效率[^2]。 - **扩容机制**:默认负载因子$0.75$,容量翻倍时重新哈希[^2]。 --- #### 3. **键对象的特殊要求** - **`hashCode()`与`equals()`**:必须正确覆盖这两个方法,保证: - 相等的对象必须返回相同的哈希值。 - 不相等的对象尽量返回不同的哈希值(减少冲突)[^3]。 **示例:自定义键对象** ```java class Student { String id; @Override public int hashCode() { return id.hashCode(); } @Override public boolean equals(Object obj) { return obj instanceof Student && this.id.equals(((Student)obj).id); } } ``` --- #### 4. **进阶操作:排序** HashMap本身无序,但可通过以下方式排序: ```java // 按键排序 List<String> sortedKeys = new ArrayList<>(map.keySet()); Collections.sort(sortedKeys); // 按值排序(需借助Stream API) List<Map.Entry<String, Integer>> sortedEntries = map.entrySet() .stream() .sorted(Map.Entry.comparingByValue()) .collect(Collectors.toList()); ``` 此方法利用了列表和比较器实现排序[^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值