Java哈希表入门详解(Hash)

一、什么是哈希表?

想象一下你正在图书馆里。

  • 数组:就像一排编号从0到N的书架。如果你知道你想找的书在第100号书架,你可以直接走过去拿到它,非常快(这就是数组的"随机访问")。但如果你只知道书名,你就得从第一个书架开始一个一个找,直到找到为止,这非常慢(这就是数组按内容查找的缺点,时间复杂度 O(n))。

  • 哈希表 :就像一个非常聪明的图书管理员。你只需要告诉他书名(比如 《Java编程思想》 ),他瞬间就能告诉你这本书在 "J-15" 这个书架上。

这个"聪明的图书管理员"就是哈希函数(Hash Function)

二、哈希表的核心机制

1. 哈希函数 (Hash Function)

一个好的哈希函数应该:

  • 确定性:相同的键必须总是产生相同的哈希值。

  • 高效:计算速度要快。

  • 均匀分布:能将不同的键均匀地映射到不同的索引上,减少"冲突"。

在 Java 中,每个对象都有一个 .hashCode() 方法,它返回一个 int 类型的哈希值。哈希表内部就是利用这个方法来定位的。

2. 哈希冲突 (Hash Collision)

冲突是不可避免的。因为哈希值是一个 int,而键的可能性是无限的(比如字符串可以是任意长度),所以不同的键完全有可能计算出相同的哈希值(从而得到相同的数组索引)。

例如:"Aa""BB" 这两个字符串的 .hashCode() 在 Java 中都是 2112

那么如何处理冲突呢?主要有两种方法:

a) 链地址法 (Separate Chaining)

- Java HashMap 采用的方法

数组的每个桶里不再直接存储值,而是存储一个链表 (或一棵红黑树)的头节点。当发生冲突时,新的键值对会被添加到对应桶的链表(或树)中。

b) 开放地址法 (Open Addressing)

当发生冲突时,它会寻找数组中的"下一个"空桶,直到找到空位为止。寻找下一个位置的方法有线性探测、二次探测等。Java 中的 ThreadLocalMap 使用了这种方法。

三、Java 中的实现:HashMap

1.如何理解HashMap

在 Java 中,最常用、最重要的哈希表实现是 HashMap 类(位于 java.util 包中)。

我们可以把 HashMap 想象成一个**"字典"** 或者一个**"储物柜"**。

  • 字典 :你通过一个"字"(称为 Key,键 )来查找这个字的"解释"(称为 Value,值)。

  • 储物柜 :你通过一个"柜号"(Key )来存取你存放的"物品"(Value)。

HashMap 就是这样一个存储"键值对(Key-Value Pairs)"的容器。它的设计初衷就是为了能通过 键(Key) 来快速查询插入删除 其对应的值(Value)

2.核心特性

  1. 键不可重复 :同一个 HashMap 中,每个键(Key)都是唯一的。如果你放入两个相同的 Key,后一个 Value 会覆盖前一个。

  2. 允许 null 键和 null 值HashMap 允许你使用 null 作为 Key,也允许使用 null 作为 Value。

  3. 无序HashMap 不记录元素的插入顺序,也不会自己进行排序。遍历它的时候,顺序是不可预测的。(如果你需要有序,可以使用 LinkedHashMap)。

  4. 非线程安全HashMap 不是为多线程环境设计的。如果多个线程同时操作同一个 HashMap 且没有做同步处理,可能会导致数据不一致。在多线程环境下应使用 ConcurrentHashMap

3.HashMap 的创建

在使用前,需要先导入它所在的包(Java 集合框架的一部分):

java 复制代码
import java.util.HashMap;

创建 HashMap 对象的语法:

java 复制代码
// 基本语法:Key 和 Value 的类型需要指定
HashMap<KeyType, ValueType> map = new HashMap<>();

// 示例:创建一个 Key 是 String 类型,Value 是 Integer 类型的 HashMap
HashMap<String, Integer> priceMap = new HashMap<>();

// 也可以在创建时指定初始容量(可选的优化参数,初学者可先忽略)
HashMap<String, String> userMap = new HashMap<>(20);

4.基本常用方法(CRUD)

CRUD 代表 Create(创建)、Read(读取)、Update(更新)、Delete(删除),这是最核心的操作。

假设我们创建一个管理水果价格的 Map:

java 复制代码
HashMap<String, Integer> fruitPrices = new HashMap<>();

(1) 添加/更新元素:put(K key, V value)

java 复制代码
// 添加键值对
fruitPrices.put("Apple", 5);   // -> {Apple=5}
fruitPrices.put("Banana", 3);  // -> {Apple=5, Banana=3}
fruitPrices.put("Orange", 4);  // -> {Apple=5, Banana=3, Orange=4}

// 更新:使用已存在的 Key 放入新的 Value
fruitPrices.put("Apple", 6);   // -> {Apple=6, Banana=3, Orange=4}
// Apple 的价格从 5 更新为 6

(2) 获取元素:get(Object key)

java 复制代码
// 通过 Key 来获取对应的 Value
int applePrice = fruitPrices.get("Apple"); // applePrice = 6
int bananaPrice = fruitPrices.get("Banana"); // bananaPrice = 3

// 如果获取一个不存在的 Key,会返回 null
Integer unknownPrice = fruitPrices.get("Mango"); // unknownPrice = null

// 注意:如果用 int 类型接收 null,会抛出 NullPointerException
// 所以更安全的做法是使用 Integer 类型接收,或者先判断

(3)判断键是否存在:containsKey(Object key)

java 复制代码
// 在获取之前,最好先检查 Key 是否存在,避免 NullPointerException
if (fruitPrices.containsKey("Mango")) {
    System.out.println("Mango price: " + fruitPrices.get("Mango"));
} else {
    System.out.println("We don't have mango.");
}

(4) 删除元素:remove(Object key)

java 复制代码
// 删除指定 Key 对应的键值对
fruitPrices.remove("Orange"); // -> {Apple=6, Banana=3}

// 键值对 "Orange=4" 被移除了

(5) 获取大小:size()

java 复制代码
// 返回 Map 中键值对的数量
int size = fruitPrices.size(); // size = 2

(6) 判断是否为空:isEmpty()

java 复制代码
// 判断 Map 是否没有任何键值对
boolean isEmpty = fruitPrices.isEmpty(); // isEmpty = false

(7)清空所有元素:clear()

java 复制代码
fruitPrices.clear(); // -> {}
System.out.println(fruitPrices.isEmpty()); // 输出 true

5.遍历 HashMap

遍历是非常常见的操作,有几种主要方式:

假设我们有如下 Map:

java 复制代码
HashMap<String, Integer> fruitPrices = new HashMap<>();
fruitPrices.put("Apple", 5);
fruitPrices.put("Banana", 3);
fruitPrices.put("Orange", 4);

方法 1:遍历所有的 Key --- keySet()

keySet() 方法返回一个包含所有 Key 的 Set 集合。

java 复制代码
for (String fruit : fruitPrices.keySet()) {
    System.out.println("Fruit: " + fruit);
    // 通过 Key 可以再获取 Value
    System.out.println("Price: " + fruitPrices.get(fruit));
}

方法 2:遍历所有的 Value --- values()

values() 方法返回一个包含所有 Value 的 Collection 集合。(不常用,因为丢失了 Key 的信息)

java 复制代码
for (Integer price : fruitPrices.values()) {
    System.out.println("Price: " + price);
}

方法 3(最常用、最高效):遍历所有的键值对 --- entrySet()

entrySet() 方法返回一个包含所有"键值对入口"(Map.Entry 对象)的 Set 集合。每个 Entry 对象都有 getKey()getValue() 方法。

java 复制代码
for (HashMap.Entry<String, Integer> entry : fruitPrices.entrySet()) {
    String key = entry.getKey();
    Integer value = entry.getValue();
    System.out.println(key + " costs $" + value);
}
// 输出:
// Apple costs $5
// Banana costs $3
// Orange costs $4

为什么推荐这个方法? 因为在遍历时直接拿到了 Key 和 Value,无需再通过 get(key) 去查询,效率更高。

6.一个完整的代码示例

java 复制代码
import java.util.HashMap;

public class HashMapDemo {
    public static void main(String[] args) {
        // 1. 创建 HashMap
        HashMap<String, String> capitalCities = new HashMap<>();

        // 2. 添加键值对
        capitalCities.put("England", "London");
        capitalCities.put("Germany", "Berlin");
        capitalCities.put("Norway", "Oslo");
        capitalCities.put("USA", "Washington DC");
        System.out.println("Initial Map: " + capitalCities); // 直接打印整个Map

        // 3. 获取元素
        String capitalOfGermany = capitalCities.get("Germany");
        System.out.println("Capital of Germany is: " + capitalOfGermany);

        // 4. 删除元素
        capitalCities.remove("England");
        System.out.println("After removing England: " + capitalCities);

        // 5. 检查大小和是否为空
        System.out.println("Size: " + capitalCities.size());
        System.out.println("Is empty? " + capitalCities.isEmpty());

        // 6. 检查键是否存在
        if (capitalCities.containsKey("USA")) {
            System.out.println("USA is in the map.");
        }

        // 7. 遍历 Map (推荐方式)
        System.out.println("\nTraversing the map:");
        for (HashMap.Entry<String, String> entry : capitalCities.entrySet()) {
            System.out.println("Country: " + entry.getKey() + " -> Capital: " + entry.getValue());
        }

        // 8. 清空 Map
        capitalCities.clear();
        System.out.println("After clear: " + capitalCities);
    }
}

7、hashmap总结

  1. 创建HashMap<KeyType, ValueType> map = new HashMap<>();

  2. 增/改map.put(key, value);

  3. map.get(key);

  4. map.remove(key);

  5. 遍历首选 for (Map.Entry<KeyType, ValueType> entry : map.entrySet()) { ... }

  6. 判断存在map.containsKey(key)

四、哈希表总结

  1. 首选 HashMap :在绝大多数不需要线程安全的场景下,都使用 HashMap

  2. 正确重写 hashCode()equals() :如果你要用自定义的类 (比如 Student, Employee)作为 HashMap 的键,你必须 同时重写这个类的 hashCode()equals() 方法。

    • hashCode() 决定了键值对存放在哪个桶里。

    • equals() 用于在发生哈希冲突时,在链表中比较两个键是否真正相等。

    • 规则:如果两个对象通过 equals() 比较是相等的,那么它们的 hashCode() 也必须相等。

  3. 理解基本原理 :明白哈希、冲突、扩容这些概念,能帮助你更好地使用和理解 HashMap 的行为,而不是仅仅死记硬背API。

相关推荐
海绵宝宝的好伙伴3 小时前
【数据结构】哈希表的理论与实现
数据结构·哈希算法·散列表
Aqua Cheng.3 小时前
代码随想录第七天|哈希表part02--454.四数相加II、383. 赎金信、15. 三数之和、18. 四数之和
java·数据结构·算法·散列表
怀揣小梦想3 小时前
跟着Carl学算法--哈希表
数据结构·c++·笔记·算法·哈希算法·散列表
Kent_J_Truman3 小时前
【模拟散列表】
数据结构·算法·蓝桥杯·散列表·常识类
努力努力再努力wz3 小时前
【C++进阶系列】:万字详解unordered_set和unordered_map,带你手搓一个哈希表!(附模拟实现unordered_set和unordered_map的源码)
java·linux·开发语言·数据结构·数据库·c++·散列表
Lchiyu3 小时前
哈希表 | 454.四数相加II 383. 赎金信 15. 三数之和 18. 四数之和
算法
对纯音乐情有独钟的阿甘3 小时前
【C++庖丁解牛】哈希表/散列表的设计原理 | 哈希函数
c++·哈希算法·散列表
励志不掉头发的内向程序员3 小时前
【STL库】哈希表的原理 | 哈希表模拟实现
开发语言·c++·学习·散列表
玩镜的码农小师兄3 小时前
[从零开始面试算法] (04/100) LeetCode 136. 只出现一次的数字:哈希表与位运算的巅峰对决
c++·算法·leetcode·面试·位运算·hot100