若业务场景强依赖键的顺序遍历或范围查询,应直接选用 TreeMap;若仅追求高频读写性能且无需顺序,HashMap 是更稳妥的选择。
先说结论:选型核心在于是否需要对键进行排序,有序选 TreeMap,无序且重性能选 HashMap。
- 适合:HashMap 适用于缓存、快速索引等无序高频存取场景;TreeMap 适用于按时间戳排序、范围查询等有序场景。
- 重点看:TreeMap 的键不允许为 null 且必须可比较(实现 Comparable 或传入 Comparator),HashMap 允许一个 null 键。
- 别忽略:TreeMap 增删查时间复杂度稳定为 O(log n),HashMap 平均为 O(1) 但极端冲突下可能退化。
- 并发替代:多线程无序场景用 ConcurrentHashMap,多线程有序场景用 ConcurrentSkipListMap。
选型决策逻辑
在代码实现前,可通过以下逻辑判断选型。注意 Java 代码中不能使用中文作为逻辑表达式,需转化为具体的业务判断:
// 决策逻辑示例
boolean needOrdered = true; // 是否需要有序遍历或范围查询
boolean keyCanBeNull = false; // 键是否可能为 null
if (needOrdered) {
// 使用 TreeMap,注意 Key 不能为 null 且需实现 Comparable
Map<K, V> map = new TreeMap<>();
} else {
// 优先使用 HashMap,性能更优
Map<K, V> map = new HashMap<>();
}完整可运行示例
以下是一个完整的 Java 测试类,演示了 HashMap 与 TreeMap 的基本用法、遍历顺序差异及 null 键处理:
import java.util.*;
public class MapSelectionDemo {
public static void main(String[] args) {
// 1. HashMap 测试
Map<Integer, String> hashMap = new HashMap<>();
hashMap.put(3, "C");
hashMap.put(1, "A");
hashMap.put(2, "B");
hashMap.put(null, "NullKey"); // HashMap 允许一个 null 键
System.out.println("--- HashMap 遍历 ---");
for (Map.Entry<Integer, String> entry : hashMap.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
// 2. TreeMap 测试
Map<Integer, String> treeMap = new TreeMap<>();
treeMap.put(3, "C");
treeMap.put(1, "A");
treeMap.put(2, "B");
// treeMap.put(null, "NullKey"); // 此行会抛出 NullPointerException
System.out.println("\n--- TreeMap 遍历 ---");
for (Map.Entry<Integer, String> entry : treeMap.entrySet()) {
System.out.println(entry.getKey() + "=" + entry.getValue());
}
// 3. 范围查询演示 (TreeMap 特有)
System.out.println("\n--- TreeMap 范围查询 (1-2) ---");
SortedMap<Integer, String> subMap = ((TreeMap<Integer, String>) treeMap).subMap(1, 3);
System.out.println(subMap);
}
}自定义排序实现
若键对象未实现 Comparable 接口,或需要自定义排序规则(如倒序),需在构造 TreeMap 时传入 Comparator:
// 自定义比较器:按字符串长度排序
Map<String, Integer> customMap = new TreeMap<>(new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return Integer.compare(s1.length(), s2.length());
}
});
// Java 8 Lambda 简化写法
Map<String, Integer> lambdaMap = new TreeMap<>((s1, s2) -> s2.compareTo(s1)); // 倒序性能验证方法
可通过简单的循环插入测试观察耗时差异。注意以下代码仅用于演示测试方法,具体耗时取决于硬件环境与 JVM 状态,生产环境建议使用 JMH 进行基准测试:
public void performanceTest() {
int size = 100000;
long start, end;
// HashMap 性能测试
Map<Integer, Integer> hashMap = new HashMap<>();
start = System.nanoTime();
for (int i = 0; i < size; i++) {
hashMap.put(i, i);
}
end = System.nanoTime();
System.out.println("HashMap put time: " + (end - start) / 1_000_000 + " ms");
// TreeMap 性能测试
Map<Integer, Integer> treeMap = new TreeMap<>();
start = System.nanoTime();
for (int i = 0; i < size; i++) {
treeMap.put(i, i);
}
end = System.nanoTime();
System.out.println("TreeMap put time: " + (end - start) / 1_000_000 + " ms");
}并发场景替代方案
两者均非线程安全。在多线程环境下,需根据有序性需求选择并发容器:
- 无序高并发:使用
ConcurrentHashMap替代 HashMap,性能优于 Collections.synchronizedMap。 - 有序高并发:使用
ConcurrentSkipListMap替代 TreeMap。它是基于跳表实现的并发有序 Map,支持并发读写且保持键有序。
// 并发有序场景推荐
ConcurrentNavigableMap<String, String> concurrentMap = new ConcurrentSkipListMap<>();常见坑
1. Null 键异常:TreeMap 插入 null 键会直接抛出 NullPointerException,HashMap 允许一个 null 键。若数据源可能包含 null,需提前过滤或改用 HashMap。
2. 遍历顺序误解:HashMap 不保证遍历顺序与插入顺序一致,JDK 1.8 后虽优化了部分重排逻辑,但仍不可依赖其顺序性。若需保持插入顺序,请使用 LinkedHashMap。
3. 比较器一致性:TreeMap 若使用自定义 Comparator,需确保比较逻辑与 equals 方法一致(即 compare(a,b)==0 当且仅当 a.equals(b)),否则可能导致查找失败或行为异常。
4. 内存占用:TreeMap 因维护树结构节点(Entry 对象包含左右子节点引用),内存占用通常比 HashMap 略高,内存敏感场景需权衡。
参考来源
- Oracle Java SE Documentation: java.util.HashMap
- Oracle Java SE Documentation: java.util.TreeMap
- Oracle Java SE Documentation: java.util.concurrent.ConcurrentSkipListMap
- OpenJDK Source Code: java.util.TreeMap.java