在 Java 中,HashMap 是一种基于哈希表实现的键值对存储结构,它继承自 AbstractMap 类并实现了 Map 接口。HashMap 允许使用 null 作为键和值,并且不保证元素的顺序。下面详细介绍 HashMap 的特性、用法及内部原理。
HashMap 的核心特性
无序性:元素的存储顺序与插入顺序无关。
唯一性:键不能重复,若插入相同的键,新值会覆盖旧值。
允许 null:键和值均可为 null,但键的 null 只能有一个。
非线程安全:适用于单线程环境;多线程环境下建议使用 ConcurrentHashMap。
高效性:基于哈希表,基本操作(put、get)的时间复杂度为 O(1)。
HashMap 的常用方法
以下是 HashMap 的核心方法示例:
import java.util.HashMap;
import java.util.Map;
public class HashMapExample {
public static void main(String[] args) {
// 创建 HashMap
Map<String, Integer> scores = new HashMap<>();
// 添加元素
scores.put("Alice", 90);
scores.put("Bob", 85);
scores.put("Charlie", 95);
scores.put(null, 100); // 允许 null 键
scores.put("David", null); // 允许 null 值
// 获取元素
System.out.println("Alice 的分数: " + scores.get("Alice")); // 90
System.out.println("null 键的值: " + scores.get(null)); // 100
// 检查键/值是否存在
System.out.println("是否包含 Bob: " + scores.containsKey("Bob")); // true
System.out.println("是否包含值 95: " + scores.containsValue(95)); // true
// 删除元素
scores.remove("Charlie");
System.out.println("删除 Charlie 后: " + scores);
// 遍历方式一:使用 entrySet
System.out.println("遍历方式一(entrySet):");
for (Map.Entry<String, Integer> entry : scores.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 遍历方式二:使用 keySet
System.out.println("遍历方式二(keySet):");
for (String key : scores.keySet()) {
System.out.println(key + ": " + scores.get(key));
}
// 遍历方式三:使用 forEach + Lambda
System.out.println("遍历方式三(forEach + Lambda):");
scores.forEach((key, value) -> System.out.println(key + ": " + value));
// 获取大小
System.out.println("HashMap 大小: " + scores.size()); // 4
}
}
HashMap 的内部原理
哈希表结构
HashMap 使用数组 + 链表 / 红黑树实现。当发生哈希冲突时,元素会以链表形式存储;当链表长度超过 8 且数组长度超过 64 时,链表会转换为红黑树以提高查询效率。
哈希冲突处理
通过链地址法(Separate Chaining)解决冲突,即多个元素映射到同一位置时,以链表或树结构存储。
扩容机制
初始容量为 16,负载因子(Load Factor)默认为 0.75。
当元素数量超过 容量 × 负载因子 时,会触发扩容(Resize),容量翻倍并重新哈希。
自定义对象作为键的注意事项
若使用自定义对象作为键,必须重写 hashCode() 和 equals() 方法:
hashCode():
确保相同对象返回相同哈希值。
equals():
定义对象相等的逻辑。
import java.util.HashMap;
import java.util.Objects;
class Student {
private final String name;
private final int id;
public Student(String name, int id) {
this.name = name;
this.id = id;
}
// 重写 hashCode()
@Override
public int hashCode() {
return Objects.hash(name, id);
}
// 重写 equals()
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return id == student.id && name.equals(student.name);
}
@Override
public String toString() {
return "Student{name='" + name + "', id=" + id + "}";
}
}
public class CustomKeyExample {
public static void main(String[] args) {
HashMap<Student, Integer> studentScores = new HashMap<>();
studentScores.put(new Student("Alice", 1), 90);
studentScores.put(new Student("Bob", 2), 85);
// 通过相同属性的对象获取值(必须重写 hashCode 和 equals)
System.out.println("Alice 的分数: " + studentScores.get(new Student("Alice", 1))); // 90
}
}
HashMap vs. Hashtable vs. ConcurrentHashMap
特性 HashMap Hashtable ConcurrentHashMap
线程安全 否 是(全同步) 是(分段锁 / CAS)
null 键 / 值 允许 不允许 不允许
性能 高 低(全同步开销) 高(并发优化)
适用场景 单线程 多线程(已过时) 多线程
常见面试问题
HashMap 的工作原理
基于哈希表,通过 hashCode() 定位桶位置,通过 equals() 比较键。
为什么负载因子是 0.75
权衡空间和时间效率:过小会频繁扩容,过大会增加哈希冲突概率。
HashMap 是否线程安全?如何替代?
不安全,多线程环境下使用 ConcurrentHashMap。
HashMap 在 JDK 8 有哪些优化?
引入红黑树,优化哈希冲突时的性能(链表转树)。
掌握 HashMap 的核心原理和用法是 Java 开发的基础,它广泛应用于缓存、数据统计、配置管理等场景。