Java —— HashMap从数据结构到扩容机制全面解析

HashMap 主要内容:

  1. 基本数据结构
  2. 树化与退化
  3. 索引计算
  4. put 与扩容
  5. 并发问题
  6. key 的设计

1. 基本数据结构🛠️

HashMap 的底层是基于数组加链表的数据结构📊。每个数组的槽位(bucket)可以存储多个键值对,这些键值对通过链表连接起来。当多个键的哈希值相同(即哈希冲突发生时),这些键值对会形成一个链表🔗。

生活例子🍔: 想象你在超市的货架上找商品,每个货架都是一个数组槽位。每个货架上可以放多个不同商品(键值对),如果商品过多,货架上的商品就用链条连接起来,一直挂下去(链表)🛒。


2. 树化与退化🌳

当一个桶(bucket)中的链表长度超过一定的阈值时(默认是8),链表会自动转变成一个红黑树🌳,这样可以提高查询效率。如果后期元素减少,树也可以退化为链表。这是为了在元素多时保持快速的查询性能,而避免链表遍历导致性能下降⏳。

生活例子🍰: 想象你在超市排队💡,如果队伍太长,店员可能会引导大家站成多个队伍,或者打开更多的收银台(树化)🏪。当顾客减少时,队伍又可以合并为一条(退化)💁‍♀️。


3. 索引计算🧮

HashMap 使用哈希函数将键(key)映射到数组中的某个位置(索引)。具体来说,HashMap 会通过 hashCode() 方法计算键的哈希值,并通过取模操作确定键值对应该放到哪个桶(bucket)里📥。

生活例子🍔: 想象你有一张超市的地图🛒,每个货架上都有一个编号(索引)。你根据商品的特性(哈希值)来决定它应该放在哪个货架上。这个货架编号就是通过索引计算出来的。


4. put 与扩容📦

每次我们调用 put() 方法往 HashMap 中放入一个键值对时,HashMap 首先计算键的哈希值,找到对应的槽位(bucket),然后将键值对存入。如果 HashMap 中的元素太多导致负载因子(load factor)超出预设值(通常是0.75),HashMap 会进行扩容,即增加数组的大小,并将所有现有的键值对重新分布🔧。

生活例子🍰: 假设你在收拾超市的货架📦,如果货架上的商品越来越多,超市可能需要增加新的货架,并把现有商品重新摆放在新货架上,以避免货架拥挤。


5. 并发问题🔗

HashMap 不是线程安全的❌,也就是说在多线程环境下同时对 HashMap 进行修改可能会导致数据不一致、死循环等问题。为了解决这个问题,Java 提供了 ConcurrentHashMap 这样的线程安全版本来处理并发场景🏗️。

生活例子🍔: 想象多个顾客同时在超市里搬商品📦,如果没有人协调,他们可能会把商品放乱,甚至会导致混乱的局面。这个时候就需要一名管理员(ConcurrentHashMap)来管理这个过程。


6. key 的设计🗝️

为了提高 HashMap 的性能,设计键(key)时要确保 hashCode()equals() 方法被正确实现。如果键的哈希值分布不均匀,会导致某些槽位(bucket)上的链表过长,从而影响性能🔧。理想情况下,每个键的哈希值都应该均匀地分布在 HashMap 中。

生活例子🍰: 就像你在超市中放商品一样,如果你把很多东西都放在同一个货架上,那货架就会特别拥挤,找东西也很慢⏳。所以最好每个货架上的商品分布均匀一些,这样大家找商品就方便多了🛒。


总结大比拼🏆:

知识点解释
基本数据结构数组 + 链表结构,当冲突发生时,链表存储多个键值对。
树化与退化链表长度超过阈值时会转换为红黑树,提升查询效率,减少时退化为链表。
索引计算哈希函数计算键的哈希值,通过索引决定键值对的存储位置。
put 与扩容插入时会根据负载因子进行扩容,重新分布现有键值对。
并发问题HashMap 在多线程下不安全,需要使用线程安全的 ConcurrentHashMap。
key 的设计设计好的键要确保 hashCode()equals() 方法,实现均匀分布。

代码展示一下:

包括 put() 操作、扩容、key 的设计等方面

HashMap 基础代码示例 + 详细注释 📝

 
import java.util.HashMap;

public class HashMapExample {

    public static void main(String[] args) {
        // 🛠️ 创建一个 HashMap,key 是字符串,value 是整数
        HashMap<String, Integer> map = new HashMap<>();

        // 📝 添加一些键值对到 HashMap 中
        // put() 方法将键值对插入到 HashMap 中
        // "Apple" 是 key,10 是 value
        map.put("Apple", 10); 
        map.put("Banana", 20);
        map.put("Orange", 30);

        // 🔍 从 HashMap 中获取值
        // get() 方法通过 key 获取对应的 value
        int appleValue = map.get("Apple");
        System.out.println("Apple 的值是: " + appleValue); // 输出: Apple 的值是: 10

        // 📝 更新一个键的值
        // put() 方法不仅用于插入,还可以用于更新
        map.put("Apple", 15); // 将 "Apple" 的值更新为 15
        System.out.println("更新后 Apple 的值是: " + map.get("Apple")); // 输出: 更新后 Apple 的值是: 15

        // 📝 检查 HashMap 中是否包含某个 key
        // containsKey() 方法返回 true 或 false
        if (map.containsKey("Banana")) {
            System.out.println("Banana 存在于 HashMap 中");
        }

        // 📝 删除键值对
        // remove() 方法通过 key 删除相应的键值对
        map.remove("Orange");
        System.out.println("删除 Orange 后的 HashMap: " + map);

        // 📝 遍历整个 HashMap
        // 使用增强型 for 循环遍历所有的键值对
        System.out.println("遍历 HashMap 的键值对:");
        for (String key : map.keySet()) {
            // keySet() 方法返回 HashMap 中所有的 key
            int value = map.get(key); // 根据 key 获取对应的 value
            System.out.println("Key: " + key + ", Value: " + value);
        }

        // 💡 模拟扩容
        // 当元素过多时,HashMap 会自动扩容,重新分配空间并重新哈希键值对
        // 这个例子模拟插入大量数据,看 HashMap 是否会自动扩容
        for (int i = 0; i < 100; i++) {
            map.put("Key" + i, i); // 添加 100 个键值对
        }
        System.out.println("插入 100 个键值对后的 HashMap 大小: " + map.size()); // 输出 HashMap 的大小
    }
}

代码逐步解释 & 注释说明💡:

  1. 创建 HashMap 🛠️:

     
    HashMap<String, Integer> map = new HashMap<>();
    
    • 创建一个 HashMap 对象,其中 key 为字符串类型 String,value 为整数类型 Integer。这是最基础的 HashMap 定义。
  2. 插入键值对📝:

     
    map.put("Apple", 10); 
    map.put("Banana", 20);
    map.put("Orange", 30);
    
    • 使用 put() 方法向 HashMap 中添加键值对。
    • put() 方法的作用是根据 key 将 value 存储起来,如果 key 已经存在,则更新其对应的值。
  3. 获取键对应的值🔍:

     
    int appleValue = map.get("Apple");
    
    • 使用 get() 方法通过 key 获取对应的 value。如果 key 不存在,则返回 null。
  4. 更新值📝:

     
    map.put("Apple", 15);
    
    • 如果 key 已经存在,再次调用 put() 方法会覆盖掉原来的值。这里把 "Apple" 的值从 10 更新为 15
  5. 检查 key 是否存在🔍:

     
    if (map.containsKey("Banana")) {
        System.out.println("Banana 存在于 HashMap 中");
    }
    
    • containsKey() 方法用来检查某个 key 是否存在于 HashMap 中,返回 truefalse
  6. 删除键值对📝:

     
    map.remove("Orange");
    
    • 使用 remove() 方法通过 key 删除对应的键值对。
  7. 遍历 HashMap 🛤️:

     
    for (String key : map.keySet()) {
        int value = map.get(key);
        System.out.println("Key: " + key + ", Value: " + value);
    }
    
    • 通过 keySet() 方法获取 HashMap 中所有的 key,并使用增强型 for 循环遍历每个键值对。
  8. 模拟扩容📦:

     
    for (int i = 0; i < 100; i++) {
        map.put("Key" + i, i);
    }
    System.out.println("插入 100 个键值对后的 HashMap 大小: " + map.size());
    
    • 当 HashMap 的大小超过负载因子(通常为 0.75)时,HashMap 会自动进行扩容。这个例子通过插入大量数据来模拟 HashMap 扩容的过程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值