HashMap工作原理

HashMap 是 Java 集合框架中最重要且最常用的数据结构之一,它基于哈希表实现了 Map 接口,提供了高效的键值对存储和检索能力。
Java 8 之后的 HashMap 采用 数组 + 链表 + 红黑树 的混合结构:

// 简化结构示意
transient Node<K,V>[] table;  // 主数组(哈希桶数组)

static class Node<K,V> {      // 链表节点
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {  // 红黑树节点
    TreeNode<K,V> parent;  
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // 保持双向链表特性
    boolean red;
}

一、工作流程

1. 存储过程(put)

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

具体步骤:
计算哈希值:对 key 进行 hash(key) 计算(高16位异或低16位)
确定桶位置:index = (n - 1) & hash(n 是数组长度)
处理碰撞:
如果桶为空:直接创建新节点插入
如果桶不为空:
如果是链表:遍历查找,存在则更新,不存在则尾插(Java8),检查是否需树化(链表长度≥8)
如果是红黑树:按照树的方式插入
扩容检查:如果元素总数超过阈值(容量×负载因子),进行扩容(2倍)

2. 读取过程(get)

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

具体步骤:
计算 key 的哈希值
确定桶位置 (n-1) & hash
在桶中查找:
如果是链表:顺序查找(O(n))
如果是红黑树:树查找(O(log n))

3. 扩容机制(resize)

当 size > threshold(容量×负载因子)时触发:

创建新数组(2倍于原容量)
重新计算所有元素的哈希位置(rehash)
迁移元素:
Java 7:头插法(可能导致死链)
Java 8+:保持原链表顺序或拆分树

二、线程安全问题

HashMap 不是线程安全的,多线程环境下可能出现:

数据不一致
Java 7 扩容时的死循环问题(已修复)
解决方案:
使用 Collections.synchronizedMap
使用 ConcurrentHashMap(推荐)

三、常见问题

1、HashMap的value是否可以传null?

HashMap允许null作为值存储在HashMap中。允许一个null作为key,如果尝试使用多个null作为key,后面的会覆盖前面的。

2、HashMap和Hashtable的区别

HashMap线程不安全,Hashtable线程安全
HashMap允许null键值,Hashtable不允许
HashMap性能更好

3、HashMap 的初始容量和负载因子是什么?

默认初始容量 16
默认负载因子 0.75
当元素数量超过(容量×负载因子)时扩容

4、HashMap 如何解决哈希冲突?

哈希冲突是指不同的键(key)经过哈希函数计算后得到相同的哈希值,从而映射到哈希表的同一个位置。HashMap 主要采用以下几种方式解决哈希冲突:

  1. 链地址法(拉链法)Java 7 及之前版本的实现方式:
    使用数组+链表的结构
    当发生哈希冲突时,将冲突的键值对以链表形式存储在同一个桶(bucket)中
    新元素插入到链表头部(头插法)
// Java 7 的简单示意结构
数组 + 链表:
[0] -> null
[1] -> Entry<K,V> -> Entry<K,V> -> null  // 哈希冲突的键值对形成链表
[2] -> null
...
  1. 红黑树优化(Java 8+)
    当链表长度超过阈值(默认为8)时,将链表转换为红黑树
    当红黑树节点数小于阈值(默认为6)时,转换回链表
    这种改进将最坏情况下的时间复杂度从O(n)降低到O(log n)
// Java 8+ 的混合结构
[0] -> null
[1] -> TreeNode<K,V>  // 转换为红黑树
[2] -> Node<K,V> -> Node<K,V> -> null  // 仍然是链表
...

5、为什么 HashMap 的长度是 2 的幂次方?

方便通过位运算计算索引:(n-1) & hash
提高计算效率,减少哈希冲突

6、HashMap 的 put 方法执行流程?

  • 计算 key 的 hash 值
  • 计算数组下标
  • 判断是否冲突
  • 插入节点(链表或红黑树)

7、为什么 Java 8 要将链表转为红黑树?

防止哈希碰撞攻击
链表过长时查询效率从 O(n) 提升到 O(logn)

8、HashMap 为什么不是线程安全的?

多线程扩容可能导致死循环
使用 ConcurrentHashMap 替代

9、HashMap 的扩容机制是怎样的?

扩容为原大小的 2 倍
重新计算所有元素的位置

10、如何优化 HashMap 的性能?

设置合理的初始容量
选择合适的负载因子
使用不可变对象作为键

11、HashMap 和 TreeMap 的区别?

HashMap 基于哈希表,无序
TreeMap 基于红黑树,有序

12、HashMap 在多线程环境下会出现什么问题?

数据不一致
死循环(Java 7 及以前版本)
推荐使用 ConcurrentHashMap

13、如何设计一个好的 hashCode 方法?

保证相同对象返回相同值
尽量使不同对象返回不同值
计算简单高效

14、HashMap 的 key 为什么通常用不可变对象?

防止 key 变化导致 hash 值变化
保证数据一致性

15、如何实现一个线程安全的 HashMap?

使用 Collections.synchronizedMap
使用 ConcurrentHashMap
使用读写锁封装

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值