HashMap 深度漫游:从入门到源码,一篇博客全吃透

目录

前言

一、什么是哈希表?

二、HashMap 快速上手

2.1创建 HashMap

2.2增加元素 (Put)

2.3查询元素 (Get)

2.4修改元素 (Update)

2.5删除元素 (Remove)

2.6遍历 HashMap

2.7关键方法总结

三、核心特性与工作原理

3.1哈希表的初始化与扩容机制

3.2 哈希函数:键到索引的魔法转换

3.3哈希冲突解决策略


前言

    在日常编程中,我们经常需要存储和快速查找成对的“键(Key)”和“值(Value)”。例如,根据用户 ID 查找用户信息,根据单词查找其定义,或者缓存计算结果。虽然数组和链表也能完成类似任务,但当数据量变大时,它们的效率往往会急剧下降(查找需要 O(n) 时间)。这时,哈希表(Hash Table) 就闪亮登场了!本文将带您从基础概念到源码实现,全面掌握Java哈希表技术。

一、什么是哈希表?

      哈希表(hashmap)是Map集合中的一员,map集合里的数据是键值对,其格式是{key1=value1,key2=value2,...}。Map集合的所有键是不允许重复的,但值可以重复,键和值是一一对应的,每一个键只能找到自己对应的值。

      接下来我们来认识哈希表,想象你开了一家杂货铺,里面有1000种商品。当顾客问起一样商品卖多少钱时,你是在一张长长的清单中挨个查找这个商品的名字和价格,还是立刻报出价格。毫无疑问你希望是后者。
     从开发的角度来看,前者的做法中,清单是数组,现在我想知道矿泉水的价格是多少,就得遍历数组,看看哪一项商品名称是它,然后就可以得到价格了。这样的方法比较耗时。
    而通过哈希表则可以实现根据商品名称立刻得到它的价格。超市的“商品 → 价格”系统就是哈希表,哈希表是一种通过哈希函数将键映射到存储位置的数据结构。

JDK8之前,哈希表=数组+链表

JDK8开始,哈希表=数组+链表+红黑树

其具体实现细节稍后讲解。

二、HashMap 快速上手

2.1创建 HashMap

import java.util.HashMap;

// 1. 创建空HashMap (默认容量16, 负载因子0.75)
HashMap<String, Integer> map1 = new HashMap<>();

// 2. 指定初始容量 (避免频繁扩容)
HashMap<String, Double> map2 = new HashMap<>(32); // 初始容量32

// 3. 指定初始容量和负载因子
HashMap<Integer, String> map3 = new HashMap<>(50, 0.8f); // 容量50, 负载因子0.8

// 4. 从现有Map创建
Map<String, Integer> existingMap = ...;
HashMap<String, Integer> map4 = new HashMap<>(existingMap);

2.2增加元素 (Put)

HashMap<String, String> countries = new HashMap<>();

// 1. 添加键值对
countries.put("CN", "中国");
countries.put("US", "美国");
countries.put("JP", "日本");

// 2. 添加null键和null值 (允许)
countries.put(null, "未知国家"); 
countries.put("UK", null); 

// 3. putIfAbsent(): 仅当键不存在时才插入
countries.putIfAbsent("FR", "法国"); // 会插入
countries.putIfAbsent("CN", "中华人民共和国"); // 不会覆盖(键已存在)

System.out.println(countries); 
// 输出: {null=未知国家, CN=中国, FR=法国, UK=null, US=美国, JP=日本}

2.3查询元素 (Get)

// 1. 根据键获取值
String china = countries.get("CN"); // "中国"
String uk = countries.get("UK");    // null (值本身是null)
String unknown = countries.get("DE"); // null (键不存在)

// 2. getOrDefault(): 键不存在时返回默认值
String germany = countries.getOrDefault("DE", "德国"); // "德国"

// 3. 检查键是否存在
boolean hasJapan = countries.containsKey("JP"); // true
boolean hasCanada = countries.containsKey("CA"); // false

// 4. 检查值是否存在
boolean hasChina = countries.containsValue("中国"); // true
boolean hasNull = countries.containsValue(null); // true (UK的值是null)

2.4修改元素 (Update)

// 1. put() 覆盖更新 (键存在时覆盖值)
countries.put("CN", "中华人民共和国"); // 更新中国的值

// 2. replace(K, V): 仅当键存在时才更新
countries.replace("US", "美利坚合众国"); // 成功更新
countries.replace("CA", "加拿大");      // 无效果 (键不存在)

// 3. replace(K, oldValue, newValue): 键值匹配时才更新
countries.replace("JP", "日本", "日本国"); // 成功 (旧值匹配)
countries.replace("UK", "英格兰", "英国"); // 失败 (旧值不匹配)

System.out.println(countries.get("CN")); // "中华人民共和国"

2.5删除元素 (Remove)

// 1. 根据键删除
String removed = countries.remove("JP"); // 返回"日本国"并删除

// 2. 删除键值对 (键+值同时匹配才删除)
boolean isRemoved = countries.remove("UK", null); // true (UK的值是null)
boolean notRemoved = countries.remove("US", "美国"); // false (值已更新为"美利坚合众国")

// 3. 清空整个Map
countries.clear(); 
System.out.println(countries.size()); // 0

2.6遍历 HashMap

HashMap<String, Integer> population = new HashMap<>();
population.put("北京", 2154);
population.put("上海", 2487);
population.put("广州", 1868);

// 方法1: 遍历键 (keySet)
for (String city : population.keySet()) {
    System.out.println("城市: " + city);
}

// 方法2: 遍历值 (values)
for (int people : population.values()) {
    System.out.println("人口: " + people + "万");
}

// 方法3: 遍历键值对 (推荐!entrySet)
for (Map.Entry<String, Integer> entry : population.entrySet()) {
    System.out.println(entry.getKey() + "人口: " + entry.getValue() + "万");
}

// 方法4: Java 8+ forEach + Lambda
population.forEach((city, people) -> 
    System.out.println(city + "->" + people + "万")
);

2.7关键方法总结

操作

方法重要说明
创建new HashMap<>()建议预估大小初始化容量(减少扩容开销)
增/改put(key, value)键存在时覆盖旧值
get(key)键不存在返回 null
getOrDefault(key, default)安全获取值,避免空指针
remove(key)返回被删除的值
remove(key, value)键值匹配才删除
遍历entrySet() + 循环效率最高的遍历方式
forEach() + LambdaJava 8+ 简洁写法
判空isEmpty()检查是否为空Map
大小size()获取键值对数量

三、核心特性与工作原理

3.1哈希表的初始化与扩容机制

      哈希表初始化创建一个默认长度16的数组,默认加载因子为0.75
      哈希表容量总是2的幂次方(16, 32, 64...),当指定非2的幂次容量时,HashMap会自动调整为大于该值的最小2的幂,例如指定为10,实际会被调整为16。

// 1. 默认初始化:容量16,加载因子0.75
HashMap<String, Integer> defaultMap = new HashMap<>();

// 2. 指定初始容量
HashMap<String, String> sizedMap = new HashMap<>(32); // 容量32,加载因子0.75

// 3. 指定初始容量和加载因子
HashMap<Integer, String> customMap = new HashMap<>(50, 0.6f);
// 容量50,加载因子0.6,实际容量为64

     加载因子是扩容触发阈值的计算因子:阈值 = 容量 × 加载因子,当元素数量 > 容量 × 加载因子时触发扩容,扩容过程如下:

  1. 创建新数组:容量扩大为原数组的2倍

  2. 重新计算位置:遍历所有元素重新计算索引

  3. 迁移元素:将元素转移到新数组

  4. 更新参数:设置新阈值 = 新容量 × 加载因子

加载因子空间利用率查找性能扩容频率适用场景
0.5查询密集型应用
0.75通用场景(默认)
0.9内存敏感型应用

3.2 哈希函数:键到索引的魔法转换

哈希表的核心在于哈希函数,它负责将任意大小的键(Key)转换为固定范围的数组索引:

index = hash(key) % array_size

在Java的HashMap中,哈希转换过程经过精心设计:

// Java 8+ 的哈希优化实现
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

关键设计点

  1. 高位参与运算:通过^ (h >>> 16)让高位参与哈希计算,减少冲突

  2. 空键处理:null键始终映射到索引0位置

  3. 最终索引计算:(n - 1) & hash(n为桶数组大小)

3.3哈希冲突解决策略

当不同键产生相同哈希值(即相同数组索引)时,称为哈希冲突。HashMap采用分层策略解决:

第一阶段:数组+链表

1. 创建一个默认长度16的数组,默认加载因子为0.75,数组名table
2. 使用元素的哈希值对数组的长度做运算计算出应存入的位置
3. 判断当前位置是否为null,如果是null直接存入
4. 如果不为null,表示有元素,则调用equals方法比较,相等,则不存;不相等,则存入数组
   - JDK 8之前,新元素存入数组,占老元素位置,老元素挂下面
   - JDK 8开始之后,新元素直接挂在老元素下面

第二阶段:数组+链表+红黑树

JDK8开始,当链表长度超过8,且数组长度>=64时,自动将链表转成红黑树。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值