1. 概述
Java 集合框架中,HashMap、TreeMap 属于 Map 接口(键值对存储),HashSet、TreeSet 属于 Set 接口(单列元素存储,无重复)。其核心关系为:
HashSet底层依赖HashMap(元素存于HashMap的 Key,Value 用固定空对象占位);TreeSet底层依赖TreeMap(同理,元素存于TreeMap的 Key)。
本文将从 底层原理(数据结构、哈希 / 排序机制、扩容 / 平衡策略)、核心用法、底层运行过程演示 三方面逐一解析,并最终给出对比总结。
2. HashMap
2.1 底层原理
2.1.1 数据结构(JDK 1.8+)
采用 数组 + 链表 + 红黑树 混合结构:
- 数组(哈希桶):核心存储结构,类型为
Node<K,V>[] table,默认初始容量 16(必须是 2 的幂,保证哈希计算高效); - 链表:解决哈希冲突(不同 Key 计算出相同索引),当链表长度超过阈值(默认 8)且数组长度 ≥ 64 时,转为红黑树;
- 红黑树:优化长链表的查询效率(链表查询时间复杂度 O (n),红黑树 O (log n)),当树节点数少于 6 时,退化为链表。
2.1.2 哈希机制
-
哈希值计算:Key 的
hashCode()经过扰动函数(HashMap.hash()方法)处理,减少哈希冲突概率:static final int hash(Object key) { int h; // key为null时hash值为0,固定存于数组索引0 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } -
索引计算:通过
(table.length - 1) & hash得到数组索引(因数组长度是 2 的幂,&等价于取模,效率更高)。
2.1.3 扩容与树化策略
- 核心参数:
- 初始容量:16(
DEFAULT_INITIAL_CAPACITY); - 负载因子:0.75(
DEFAULT_LOAD_FACTOR),平衡空间与时间效率; - 阈值(threshold):
容量 × 负载因子,触发扩容的临界值。
- 初始容量:16(
- 扩容机制:
- 当
size(实际元素数)> threshold时,扩容为原容量的 2 倍(保证仍是 2 的幂); - 扩容时需重新计算所有元素的索引(JDK 1.8 优化:通过高位哈希值判断,无需重复计算 hash),并迁移至新数组。
- 当
- 树化条件:
- 链表长度 ≥ 8(
TREEIFY_THRESHOLD); - 数组长度 ≥ 64(
MIN_TREEIFY_CAPACITY); - 若数组长度不足 64,仅触发扩容而非树化。
- 链表长度 ≥ 8(
2.2 核心用法
2.2.1 构造方法
// 1. 默认构造(容量16,负载因子0.75)
HashMap<K,V> map = new HashMap<>();
// 2. 指定初始容量
HashMap<K,V> map = new HashMap<>(32);
// 3. 指定初始容量和负载因子
HashMap<K,V> map = new HashMap<>(32, 0.8f);
// 4. 从其他Map复制
HashMap<K,V> map = new HashMap<>(otherMap);
2.2.2 常用 API
| 方法 | 功能描述 |
|---|---|
put(K key, V val) | 新增 / 覆盖键值对(返回旧值) |
get(Object key) | 根据 Key 获取 Value(无则返回 null) |
remove(Object key) | 删除键值对(返回旧值) |
containsKey(Object key) | 判断是否包含指定 Key |
keySet() | 获取所有 Key 的 Set 集合 |
entrySet() | 获取键值对 Entry 的 Set 集合 |
size() | 获取元素个数 |
2.3 底层运行过程演示
目标:演示哈希冲突、链表、扩容、树化的完整过程
import java.lang.reflect.Field;
import java.util.HashMap;
public class HashMapDemo {
// 自定义Key,重写hashCode制造哈希冲突
static class ConflictKey {
private int id;
public ConflictKey(int id) {
this.id = id;
}
// 让id%3相同的Key哈希值相同,强制冲突
@Override
public int hashCode() {
return id % 3;
}
// 仅当id相同时视为同一Key
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o) return false;
ConflictKey that = (ConflictKey) o;
return id == that.id;
}
@Override
public String toString() {
return "Key{" + "id=" + id + "}";
}
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
HashMap<ConflictKey, String> map = new HashMap<>();
Field tableField = HashMap.class.getDeclaredField("table");
tableField.setAccessible(true); // 反射获取table数组
// 第一步:放入9个元素,制造哈希冲突(索引0、1、2各3个元素)
System.out.println("=== 第一步:放入9个冲突元素 ===");
for (int i = 1; i <= 9; i++) {
map.put(new ConflictKey(i), "val" + i);
}
Object[] table = (Object[]) tableField.get(map);
System.out.println("数组长度:" + table.length); // 初始16(未扩容,size=9 < 16*0.75=12)
System.out.println("索引1的桶(链表):");
traverseNode((HashMap.Node) table[1]); // 输出Key{id=1} -> Key{id=4} -> Key{id=7}
// 第二步:再放入5个元素,触发扩容(size=14 > 12)
System.out.println("\n=== 第二步:放入5个元素,触发扩容 ===");
for (int i = 10; i <= 14; i++) {
map.put(new ConflictKey(i), "val" + i);
}
table = (Object[]) tableField.get(map);
System.out.println("扩容后数组长度:" + table.length); // 32(16*2)
// 第三步:继续放入元素,触发树化(索引1的链表长度≥8,数组长度≥64)
System.out.println("\n=== 第三步:放入足够元素,触发树化 ===");
for (int i = 15; i <= 30; i++) {
map.put(new ConflictKey(i), "val" + i);
}
table = (Object[]) tableField.get(map);
System.out.println("最终数组长度:" + table.length); // 64(32*2)
System.out.println("索引1的桶类型:" + table[1].getClass().getSimpleName()); // TreeNode(红黑树节点)
}
// 遍历链表节点
private static void traverseNode(HashMap.Node node) {
while (node != null) {
System.out.print(node.getKey() + " -> ");
node = node.next;
}
System.out.println("null");
}
}
运行结果解析:
- 初始阶段:数组长度 16,3 个 Key 共享一个索引(哈希冲突),形成链表;
- 扩容阶段:元素数超过阈值 12,数组扩容为 32;
- 树化阶段:继续添加元素后,数组长度达到 64,且索引 1 的链表长度 ≥8,自动转为红黑树。
3. TreeMap
3.1 底层原理
3.1.1 数据结构
采用 红黑树(自平衡的二叉搜索树)作为唯一存储结构,每个节点为 TreeNode<K,V>,包含:
- Key、Value;
- 左子树(left)、右子树(right)、父节点(parent);
- 颜色标记(red/black),用于维持树的平衡。
3.1.2 排序机制
TreeMap 是 有序 Map,排序规则二选一(必须指定,否则 Key 需实现 Comparable):
- 自然排序:Key 实现
Comparable<K>接口,重写compareTo(K o)方法(如 String、Integer 等内置类); - 定制排序:构造方法传入
Comparator<K>接口实现类,自定义排序逻辑。
3.1.3 平衡策略
红黑树通过以下规则保证平衡(插入 / 删除后触发):
- 每个节点非红即黑;
- 根节点为黑;
- 所有叶子节点(NIL)为黑;
- 红色节点的子节点必为黑;
- 从任一节点到其叶子节点的所有路径,黑节点数相同。
若违反规则,通过 旋转(左旋 / 右旋) 和 变色 调整,确保树的高度维持在 O (log n),保证查询 / 插入 / 删除效率。
3.1.4 无扩容概念
红黑树是动态调整的树形结构,无需数组扩容,节点随元素增删动态插入 / 删除并自平衡。
3.2 核心用法
3.2.1 构造方法
// 1. 自然排序(Key必须实现Comparable)
TreeMap<K,V> treeMap = new TreeMap<>();
// 2. 定制排序(传入Comparator)
TreeMap<K,V> treeMap = new TreeMap<>(Comparator.comparingInt(User::getAge));
// 3. 从其他Map复制(继承排序规则)
TreeMap<K,V> treeMap = new TreeMap<>(otherSortedMap);
3.2.2 常用 API(新增排序相关方法)
| 方法 | 功能描述 |
|---|---|
put(K key, V val) | 按排序规则插入键值对(Key 重复则覆盖) |
get(Object key) | 根据 Key 获取 Value |
firstKey() | 获取最小 Key(排序后第一个) |
lastKey() | 获取最大 Key(排序后最后一个) |
subMap(K fromKey, K toKey) | 获取区间 [fromKey, toKey) 的子 Map |
headMap(K toKey) | 获取 Key < toKey 的子 Map |
tailMap(K fromKey) | 获取 Key ≥ fromKey 的子 Map |
3.3 底层运行过程演示
目标:演示自然排序、定制排序及红黑树平衡特性
import java.util.TreeMap;
public class TreeMapDemo {
// 自定义User类,用于演示排序
static class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
// getter
public int getAge() { return age; }
public String getName() { return name; }
@Override
public String toString() {
return "User{name='" + name + "', age=" + age + "}";
}
}
public static void main(String[] args) {
// 1. 定制排序:按年龄升序(年龄相同则按姓名字典序)
TreeMap<User, String> ageSortedMap = new TreeMap<>((u1, u2) -> {
if (u1.getAge() != u2.getAge()) {
return u1.getAge() - u2.getAge(); // 年龄升序
}
return u1.getName().compareTo(u2.getName()); // 姓名字典序
});
// 插入元素(触发红黑树插入与平衡)
ageSortedMap.put(new User("Bob", 22), "设计师");
ageSortedMap.put(new User("Alice", 25), "工程师");
ageSortedMap.put(new User("David", 25), "测试工程师"); // 年龄相同,按姓名排序
ageSortedMap.put(new User("Charlie", 30), "产品经理");
System.out.println("=== 按年龄升序遍历(红黑树有序输出)===");
ageSortedMap.forEach((user, job) -> System.out.println(user + " -> " + job));
// 输出顺序:Bob(22) → Alice(25) → David(25) → Charlie(30)
// 2. 自然排序:String按字典序
TreeMap<String, Integer> strSortedMap = new TreeMap<>();
strSortedMap.put("banana", 3);
strSortedMap.put("apple", 1);
strSortedMap.put("cherry", 5);
System.out.println("\n=== String自然排序(字典序)===");
System.out.println("最小Key:" + strSortedMap.firstKey()); // apple
System.out.println("最大Key:" + strSortedMap.lastKey()); // cherry
System.out.println("区间[apple, cherry):" + strSortedMap.subMap("apple", "cherry")); // {apple=1, banana=3}
}
}
运行结果解析:
- 定制排序中,元素按年龄升序排列,年龄相同时按姓名字典序,体现红黑树的有序性;
- 自然排序中,String 按字典序排列,通过
firstKey()、subMap()等方法可高效获取有序子集,底层依赖红黑树的快速查找特性。
4. HashSet
4.1 底层原理
4.1.1 数据结构
完全依赖 HashMap:
HashSet内部维护一个HashMap实例(map);- 元素存储在
map的 Key 位置; - Value 固定为一个静态常量
PRESENT = new Object()(占位,无实际意义)。
因此,HashSet 的数据结构、哈希机制、扩容策略与 HashMap 完全一致(数组 + 链表 + 红黑树)。
4.1.2 去重原理
依赖 HashMap 的 Key 去重规则:
- 先计算元素的
hashCode(),判断是否存在哈希冲突; - 若哈希值不同,直接插入;
- 若哈希值相同,通过
equals()方法判断是否为同一元素; - 若
equals()返回true,视为重复元素,不插入;否则插入链表 / 红黑树。
4.2 核心用法
4.2.1 构造方法
// 1. 默认构造(底层HashMap容量16,负载因子0.75)
HashSet<E> set = new HashSet<>();
// 2. 指定初始容量
HashSet<E> set = new HashSet<>(32);
// 3. 指定初始容量和负载因子
HashSet<E> set = new HashSet<>(32, 0.8f);
// 4. 从其他Collection复制
HashSet<E> set = new HashSet<>(otherCollection);
4.2.2 常用 API
| 方法 | 功能描述 |
|---|---|
add(E e) | 添加元素(重复则返回 false) |
remove(Object o) | 删除元素(存在则返回 true) |
contains(Object o) | 判断是否包含元素 |
size() | 获取元素个数 |
isEmpty() | 判断是否为空 |
4.3 底层运行过程演示
目标:演示去重机制与哈希冲突处理
import java.util.HashSet;
import java.util.Objects;
public class HashSetDemo {
// 自定义Student类,重写hashCode和equals实现去重
static class Student {
private String studentId;
private String name;
public Student(String studentId, String name) {
this.studentId = studentId;
this.name = name;
}
// 关键:studentId相同则哈希值相同
@Override
public int hashCode() {
return Objects.hash(studentId);
}
// 关键:studentId相同则视为同一学生(去重依据)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o) return false;
Student student = (Student) o;
return Objects.equals(studentId, student.studentId);
}
@Override
public String toString() {
return "Student{id='" + studentId + "', name='" + name + "'}";
}
}
public static void main(String[] args) {
HashSet<Student> studentSet = new HashSet<>();
// 添加3个学生,其中2个studentId相同(视为重复)
studentSet.add(new Student("001", "张三"));
studentSet.add(new Student("002", "李四"));
boolean addResult = studentSet.add(new Student("001", "张三三")); // 重复,添加失败
System.out.println("=== HashSet去重演示 ===");
System.out.println("添加重复学生返回值:" + addResult); // false
System.out.println("集合大小:" + studentSet.size()); // 2(去重后)
System.out.println("集合元素:");
studentSet.forEach(System.out::println);
// 输出:Student{id='001', name='张三'}, Student{id='002', name='李四'}
// 演示哈希冲突(底层HashMap的链表/红黑树)
HashSet<ConflictKey> conflictSet = new HashSet<>();
for (int i = 1; i <= 10; i++) {
conflictSet.add(new ConflictKey(i)); // ConflictKey与HashMapDemo中一致,制造冲突
}
System.out.println("\n=== 哈希冲突处理(底层链表)===");
System.out.println("冲突集合大小:" + conflictSet.size()); // 10(无重复)
}
// 复用HashMapDemo中的ConflictKey,制造哈希冲突
static class ConflictKey {
private int id;
public ConflictKey(int id) { this.id = id; }
@Override
public int hashCode() { return id % 3; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o) return false;
ConflictKey that = (ConflictKey) o;
return id == that.id;
}
}
}
运行结果解析:
- 去重机制:
studentId="001"的两个学生被视为重复,第二个添加失败,体现hashCode()+equals()的核心作用; - 哈希冲突:
ConflictKey的hashCode()重复,但equals()不同,底层 HashMap 会将其存入同一索引的链表中,HashSet 正常存储所有元素(无重复)。
5. TreeSet
5.1 底层原理
5.1.1 数据结构
完全依赖 TreeMap:
- 内部维护一个
TreeMap实例(m); - 元素存储在
m的 Key 位置; - Value 固定为静态常量
PRESENT = new Object()(占位)。
因此,TreeSet 的数据结构(红黑树)、排序机制、平衡策略与 TreeMap 完全一致。
5.1.2 去重原理
依赖 TreeMap 的 Key 去重规则:
- 基于排序规则判断元素是否重复:
- 自然排序:通过
compareTo(K o)方法,返回 0 则视为重复; - 定制排序:通过
Comparator.compare(K o1, K o2)方法,返回 0 则视为重复。
- 自然排序:通过
5.2 核心用法
5.2.1 构造方法
// 1. 自然排序(元素必须实现Comparable)
TreeSet<E> set = new TreeSet<>();
// 2. 定制排序(传入Comparator)
TreeSet<E> set = new TreeSet<>(Comparator.comparingInt(String::length));
// 3. 从其他Collection复制
TreeSet<E> set = new TreeSet<>(otherCollection);
5.2.2 常用 API(新增排序相关方法)
| 方法 | 功能描述 |
|---|---|
add(E e) | 按排序规则添加元素(重复则返回 false) |
remove(Object o) | 删除元素(存在则返回 true) |
first() | 获取最小元素 |
last() | 获取最大元素 |
subSet(E fromElement, E toElement) | 获取区间 [from, to) 的子集 |
headSet(E toElement) | 获取元素 < toElement 的子集 |
5.3 底层运行过程演示
目标:演示排序与去重机制
import java.util.TreeSet;
public class TreeSetDemo {
public static void main(String[] args) {
// 1. 定制排序:按字符串长度降序(长度相同则按字典序)
TreeSet<String> lengthSortedSet = new TreeSet<>((s1, s2) -> {
if (s1.length() != s2.length()) {
return s2.length() - s1.length(); // 长度降序
}
return s1.compareTo(s2); // 长度相同则字典序
});
// 添加元素(触发红黑树排序与去重)
lengthSortedSet.add("apple"); // 5
lengthSortedSet.add("banana"); // 6
lengthSortedSet.add("cherry"); // 6
lengthSortedSet.add("date"); // 4
lengthSortedSet.add("banana"); // 重复,添加失败
System.out.println("=== 按字符串长度降序遍历 ===");
lengthSortedSet.forEach(System.out::println);
// 输出:banana → cherry → apple → date
// 2. 自然排序:Integer按数值升序
TreeSet<Integer> numSet = new TreeSet<>();
numSet.add(5);
numSet.add(2);
numSet.add(8);
numSet.add(2); // 重复,添加失败
System.out.println("\n=== Integer自然排序(数值升序)===");
System.out.println("最小元素:" + numSet.first()); // 2
System.out.println("最大元素:" + numSet.last()); // 8
System.out.println("区间[2, 8):" + numSet.subSet(2, 8)); // [2,5]
}
}
运行结果解析:
- 定制排序中,元素按长度降序排列,长度相同时按字典序,重复元素(“banana”)添加失败;
- 自然排序中,Integer 按数值升序排列,通过
first()、subSet()可高效获取有序子集,底层依赖红黑树的有序性和平衡特性。
6. 核心对比总结
| 特性 | HashMap | TreeMap | HashSet | TreeSet |
|---|---|---|---|---|
| 底层结构 | 数组 + 链表 + 红黑树 | 红黑树 | 基于 HashMap(同左) | 基于 TreeMap(红黑树) |
| 有序性 | 无序(插入顺序≠遍历顺序) | 有序(自然 / 定制排序) | 无序 | 有序(自然 / 定制排序) |
| 去重依据 | Key 的 hashCode ()+equals () | Key 的 compareTo/compare () | 元素的 hashCode ()+equals () | 元素的 compareTo/compare () |
| 扩容机制 | 容量 2 倍扩容(阈值 = 容量 × 负载因子) | 无扩容(红黑树自平衡) | 同 HashMap | 无扩容(同 TreeMap) |
| 元素 / Key 限制 | Key 可 null(仅 1 个) | Key 不可 null(排序冲突) | 元素可 null(仅 1 个) | 元素不可 null(排序冲突) |
| 时间复杂度 | 插入 / 查询 / 删除 O (1)(平均) | 插入 / 查询 / 删除 O (log n) | 同 HashMap | 同 TreeMap |
| 适用场景 | 快速查询、无排序需求 | 有序查询、区间查询 | 快速去重、无排序需求 | 有序去重、区间查询 |
7. 关键注意事项
- HashMap/HashSet 去重:必须重写
hashCode()和equals(),且逻辑一致(equals()为 true 时,hashCode()必须相同); - TreeMap/TreeSet 排序:Key / 元素要么实现
Comparable,要么构造时传入Comparator,否则抛出ClassCastException; - HashMap 负载因子:默认 0.75,值越大空间利用率越高但冲突概率越大,值越小冲突越少但空间浪费越多;
- 线程安全:四个类均非线程安全,多线程环境需使用
Collections.synchronizedMap()/synchronizedSet()或ConcurrentHashMap/CopyOnWriteArraySet。
Java集合框架核心类详解
2047

被折叠的 条评论
为什么被折叠?



