杰瑞科技汇

java hashmap 定义

HashMap 是 Java 集合框架中非常重要且常用的一部分,下面我将从多个角度为你全面解析它。


核心定义与概述

HashMap 是一个基于 哈希表 实现的、用于存储键值对 的集合,它继承自 AbstractMap 类,并实现了 Map 接口。

核心特点:

  1. 键值对存储:每个元素都包含一个唯一的键 和一个与之对应的值,通过键可以快速找到对应的值。
  2. 允许 null 键和 null 值HashMap 中最多可以有一个 null 键,但可以有多个 null 值。
  3. 无序性HashMap 不保证元素的存储顺序,你遍历它时,元素的顺序可能与插入顺序不同,也可能每次遍历的顺序都不同(在 Java 8 之前尤其如此,Java 8 引入了 TreeNode 后,顺序稍微稳定一些,但仍不保证插入顺序)。
  4. 线程不安全HashMap 不是线程安全的,如果在多线程环境下对 HashMap 进行并发修改,可能会导致数据不一致或死循环等问题,如果需要在多线程环境下使用,应该使用 ConcurrentHashMapHashtable
  5. 高效性:在理想情况下,HashMap 的增、删、改、查操作的时间复杂度接近 O(1),这是它最大的优势。

底层实现原理

理解 HashMap 的底层原理是掌握它的关键,其核心是 “数组 + 链表 + 红黑树” 的组合结构,也称为 哈希桶

核心概念

  • Node<K,V>HashMap 中的基本数据单元,是一个内部类,代表了存储在哈希表中的一个键值对,它包含 hash(键的哈希值)、keyvaluenext(指向下一个 Node 的指针)。
  • 哈希表:一个 Node<K,V>[] 类型的数组,我们称之为 “桶数组” (Table),这是 HashMap 存储数据的核心。
  • 哈希函数:将任意 Objectkey)转换成一个整数(hash 值)的函数,在 Java 中,key.hashCode() 方法返回一个初始哈希值,HashMap 会对这个值进行二次处理,以减少哈希冲突。
  • 哈希冲突:不同的 key 经过哈希函数计算后,可能得到相同的数组索引,这是不可避免的,HashMap 通过以下两种方式来解决:
    • 链地址法:在同一个索引位置上,所有冲突的 Node 以链表的形式存储。
    • 红黑树优化:当链表的长度超过一定阈值(TREEIFY_THRESHOLD,默认为 8)时,并且桶数组的长度也达到一定要求(MIN_TREEIFY_CAPACITY,默认为 64),该链表会转换成一颗平衡的红黑树,以提高查询效率(从 O(n) 降到 O(log n))。

工作流程

存储元素 (put 方法)

  1. 计算哈希值:对 keyhashCode() 进行扰动处理(异或或位运算),得到一个最终的 hash 值。
  2. 计算索引:通过公式 (n - 1) & hashn 是桶数组的长度)计算出该元素应该存放在桶数组中的哪个位置(索引)。
  3. 处理冲突
    • 桶为空:如果计算出的索引位置上没有元素(table[i] == null),则直接创建一个新的 Node 并放入该位置。
    • 桶不为空(发生冲突)
      • key 已经存在(通过 equals 方法比较),则用新值覆盖旧值,并返回旧值。
      • key 不存在,则将新的 Node 插入到链表或红黑树中。
        • 如果是链表,则采用 头插法(Java 8 之前)或 尾插法(Java 8 之后)将新节点加入。
        • 如果链表长度超过 8,并且桶数组长度 >= 64,则将链表转换为红黑树。
  4. 扩容:当 HashMap 中的元素数量(size)超过 *容量 加载因子* (`capacity loadFactor) 时,HashMap` 会进行扩容,扩容会创建一个新的、长度是原来两倍的桶数组,然后将所有旧元素重新计算索引并迁移到新数组中,这是一个非常耗时的操作。

获取元素 (get 方法)

  1. 计算哈希值和索引:与 put 方法一样,先计算 keyhash 值,再计算出它在桶数组中的索引位置。
  2. 查找元素
    • 如果该位置为空,返回 null
    • 如果不为空,比较 key 是否相等(先比较 hash,再比较 equals)。
    • 如果是链表,则遍历链表查找。
    • 如果是红黑树,则在树中查找。
  3. 返回找到的 value,如果没找到则返回 null

常用构造方法

HashMap 提供了几个常用的构造方法,让你可以自定义其容量和加载因子。

  1. HashMap()

    • 描述:创建一个空的 HashMap,默认初始容量为 16,默认加载因子为 75
    • 示例
      HashMap<String, Integer> map = new HashMap<>();
  2. HashMap(int initialCapacity)

    • 描述:创建一个具有指定初始容量的 HashMap,加载因子仍为默认的 0.75。
    • 注意:容量会被自动调整为大于等于 initialCapacity 的最小的 2 的幂次方,你传入 10,实际容量会是 16。
    • 示例
      HashMap<String, Integer> map = new HashMap>(32); // 初始容量为 32
  3. HashMap(int initialCapacity, float loadFactor)

    • 描述:创建一个具有指定初始容量和加载因子的 HashMap
    • 加载因子:它是一个衡量 HashMap 满程度的指标,默认 0.75 意味着当 HashMap 中元素数量达到容量的 75% 时,就会触发扩容,加载因子越大,空间利用率越高,但哈希冲突的概率也越大,查询效率会降低;反之,加载因子越小,冲突概率低,但空间浪费多。
    • 示例
      HashMap<String, Integer> map = new HashMap>(64, 0.5f); // 容量64,加载因子0.5

常用方法

方法 描述 示例
V put(K key, V value) 将键值对存入 Mapkey 已存在,则覆盖旧值并返回旧值;否则返回 null map.put("apple", 10);
V get(Object key) 根据键获取对应的值。key 不存在,返回 null Integer price = map.get("apple");
V remove(Object key) 移除指定 key 的键值对,并返回被移除的值。key 不存在,返回 null map.remove("apple");
boolean containsKey(Object key) 判断 Map 中是否包含指定的 key boolean hasApple = map.containsKey("apple");
boolean containsValue(Object value) 判断 Map 中是否包含指定的 value boolean hasPrice10 = map.containsValue(10);
int size() 返回 Map 中键值对的数量。 int count = map.size();
boolean isEmpty() 判断 Map 是否为空。 boolean empty = map.isEmpty();
void clear() 清空 Map 中的所有元素。 map.clear();
Set<K> keySet() 返回包含所有 keySet 集合。 Set<String> keys = map.keySet();
Collection<V> values() 返回包含所有 valueCollection 集合。 Collection<Integer> prices = map.values();
Set<Map.Entry<K,V>> entrySet() 返回包含所有键值对(Map.Entry)的 Set 集合,常用于遍历。 for (Map.Entry<String, Integer> entry : map.entrySet()) { ... }

遍历方式

遍历 HashMap 有多种方式,推荐使用 entrySet(),因为它效率最高。

使用 for-each 循环遍历 entrySet() (推荐)

HashMap<String, Integer> map = new HashMap<>();
map.put("One", 1);
map.put("Two", 2);
map.put("Three", 3);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}

使用 forEach 方法 (Java 8+)

map.forEach((key, value) -> System.out.println("Key: " + key + ", Value: " + value));

遍历 keySet()

for (String key : map.keySet()) {
    System.out.println("Key: " + key + ", Value: " + map.get(key));
}

使用迭代器遍历 entrySet()

Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
    Map.Entry<String, Integer> entry = iterator.next();
    System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
}

Hashtable 的区别

特性 HashMap Hashtable
线程安全 不安全 安全 (方法均 synchronized)
性能 低 (因为有同步开销)
Null 键/值 允许一个 null 键和多个 null 不允许 null 键或 null
父类 AbstractMap Dictionary (较老的类)
迭代器 快速失败 快速失败

HashMap 是 Java 开发中不可或缺的工具,它通过哈希表实现了高效的键值对存储。

  • 何时使用:当你需要根据一个唯一的键来快速查找、插入或删除数据时,HashMap 是首选。
  • 核心要点:理解其 数组+链表+红黑树 的结构、哈希冲突 的解决方式以及 扩容机制,能帮助你更好地使用它并排查问题。
  • 线程安全:牢记它是非线程安全的,在多线程环境下应使用 ConcurrentHashMap
分享:
扫描分享到社交APP
上一篇
下一篇