Map 选型对比:从问题到方案
先问自己三个问题
拿到需求,不要急着写代码。先回答这三个问题:
问题 1:需要排序吗?
└─ 需要 → TreeMap / ConcurrentSkipListMap
└─ 不需要 → 问题 2
问题 2:需要保持顺序吗?
└─ 需要 → LinkedHashMap
└─ 不需要 → HashMap / ConcurrentHashMap
问题 3:需要并发安全吗?
└─ 需要 → ConcurrentHashMap / ConcurrentSkipListMap
└─ 不需要 → HashMap / TreeMap / LinkedHashMap六种 Map 一张表看懂
| 实现类 | 底层 | 有序性 | key/value null | 线程安全 | 适用场景 |
|---|---|---|---|---|---|
| HashMap | 数组+链表/红黑树 | 无序 | ✅ / ✅ | ❌ | 通用场景 |
| LinkedHashMap | HashMap+链表 | 插入顺序 | ✅ / ✅ | ❌ | 保持顺序 |
| TreeMap | 红黑树 | 排序 | ❌ / ✅ | ❌ | 需要排序 |
| Hashtable | 数组+链表 | 无序 | ❌ / ❌ | ✅(全局锁) | 遗留代码 |
| ConcurrentHashMap | CAS+分段锁 | 无序 | ❌ / ❌ | ✅(分段) | 高并发 |
| WeakHashMap | HashMap+弱引用 | 无序 | ✅ / ✅ | ❌ | 缓存 |
HashMap:通用场景的首选
适用场景:不需要顺序、不需要排序,纯粹 key-value 映射。
java
// 场景 1:用户信息缓存
Map<String, User> userCache = new HashMap<>();
userCache.put(userId, fetchUser(userId));
// 场景 2:单词计数
Map<String, Integer> wordCount = new HashMap<>();
for (String word : text.split("\\s+")) {
wordCount.merge(word, 1, Integer::sum);
}
// 场景 3:对象映射
Map<Long, Product> productMap = new HashMap<>();
Product p = productMap.get(productId);LinkedHashMap:去重且保持顺序
适用场景:需要在 HashMap 的 O(1) 性能基础上,保持插入顺序。
java
// 场景 1:去除重复但保持首次出现的顺序
List<String> urls = Arrays.asList("a.com", "b.com", "a.com", "c.com");
Map<String, String> unique = new LinkedHashMap<>();
for (String url : urls) {
unique.putIfAbsent(url, url);
}
System.out.println(unique.keySet()); // [a.com, b.com, c.com]
// 场景 2:FIFO 缓存(配合 removeEldestEntry)
LinkedHashMap<String, Data> cache = new LinkedHashMap<>(16, .75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_CACHE_SIZE;
}
};TreeMap:需要排序的能力
适用场景:需要范围查询、最值查找、按 key 排序遍历。
java
// 场景 1:排行榜(按分数排序)
TreeMap<Integer, String> leaderboard = new TreeMap<>(Comparator.reverseOrder());
leaderboard.put(1500, "张三");
leaderboard.put(2000, "李四");
System.out.println(leaderboard.firstEntry()); // 最高分
// 场景 2:时间区间查找
TreeMap<LocalDate, Event> schedule = new TreeMap<>();
schedule.put(LocalDate.of(2024, 3, 15), new Event("发布"));
schedule.put(LocalDate.of(2024, 3, 20), new Event("测试"));
// 查询 3 月 16 日之后的事件
Map<LocalDate, Event> after = schedule.tailMap(LocalDate.of(2024, 3, 16));ConcurrentHashMap:高性能并发
适用场景:多线程共享 Map,且有较高并发度。
java
// 场景 1:高并发计数器
ConcurrentHashMap<String, LongAdder> counters = new ConcurrentHashMap<>();
counters.computeIfAbsent(endpoint, k -> new LongAdder()).increment();
// 场景 2:线程安全的缓存
ConcurrentHashMap<String, Future<String>> cache = new ConcurrentHashMap<>();
String result = cache.computeIfAbsent(key, k -> {
return CompletableFuture.supplyAsync(() -> heavyComputation(k));
}).get();WeakHashMap:自动清理的缓存
适用场景:key 不再被外部引用时,希望自动从 Map 中移除。
java
// 场景:缓存外部对象,不需要手动清理
WeakHashMap<ImageKey, SoftReference<Bitmap>> imageCache = new WeakHashMap<>();
// 当 ImageKey 不再被程序持有时,下一次 GC 会自动回收 entry
public Bitmap getImage(ImageKey key) {
SoftReference<Bitmap> ref = imageCache.get(key);
if (ref != null) {
Bitmap cached = ref.get();
if (cached != null) return cached;
}
Bitmap loaded = loadBitmap(key);
imageCache.put(key, new SoftReference<>(loaded));
return loaded;
}并发 Map 对比
| 类型 | 锁粒度 | 适用场景 |
|---|---|---|
Collections.synchronizedMap(hashMap) | 全局锁 | 偶尔并发 |
ConcurrentHashMap | 段锁(Java 7)/ CAS(Java 8+) | 高并发写 |
ConcurrentSkipListMap | 无锁 | 高并发写 + 需要排序 |
java
// ❌ 全局锁:所有线程竞争同一把锁
Map<String, Integer> sync = Collections.synchronizedMap(new HashMap<>());
// ✅ ConcurrentHashMap:分段锁,线程间竞争少
Map<String, Integer> concurrent = new ConcurrentHashMap<>();
// ✅ ConcurrentSkipListMap:跳表实现,支持排序 + 无锁
Map<String, Integer> sorted = new ConcurrentSkipListMap<>();不要只盯着 Map
有些场景,用其他数据结构更合适:
java
// ❌ 用 Map 统计唯一值数量
Map<String, Integer> uniqueCount = new HashMap<>();
for (String item : data) {
uniqueCount.put(item, 1); // 浪费空间
}
// 结果:只需要一个数字
// ✅ 用 Set
Set<String> unique = new HashSet<>(data);
System.out.println(unique.size());
// ❌ 用 Map 做简单的标志位
Map<String, Boolean> flags = new HashMap<>();
flags.put("enabled", true);
if (flags.getOrDefault("enabled", false)) { ... }
// ✅ 直接用常量或 Set
Set<String> enabledFeatures = new HashSet<>();
if (enabledFeatures.contains("feature")) { ... }选型决策树
需要 Map 吗?
│
├── 需要排序(按 key 比大小)
│ ├── 单线程 → TreeMap
│ └── 多线程 → ConcurrentSkipListMap
│
├── 需要保持插入顺序
│ └── LinkedHashMap
│
├── 高并发
│ └── ConcurrentHashMap
│
├── key 需要被 GC 自动清理
│ └── WeakHashMap
│
└── 其他所有场景
└── HashMap常见误区
误区 1:所有场景都用 HashMap
HashMap 不是万能的。如果需要排序,HashMap 的遍历顺序会让你困惑。
误区 2:TreeMap 比 HashMap 更「高级」
TreeMap 的 O(log n) 比 HashMap 的 O(1) 慢 5-10 倍。只有需要 TreeMap 的独有功能时才用它。
误区 3:Hashtable 是线程安全的所以更好
Hashtable 用全局锁,高并发下性能很差。ConcurrentHashMap 的分段锁/CAS 性能好得多。
误区 4:null key 和 null value 都可以随便用
HashMap 允许 null key 和 null value,但 TreeMap/ConcurrentHashMap 不允许。应该尽量避免 null,改用 getOrDefault。
总结
| 需求 | 推荐 |
|---|---|
| 通用场景,不需要顺序 | HashMap |
| 需要保持插入顺序 | LinkedHashMap |
| 需要排序/范围查询 | TreeMap |
| 高并发 | ConcurrentHashMap |
| 高并发 + 需要排序 | ConcurrentSkipListMap |
| key 需要自动清理 | WeakHashMap |
| 遗留代码 | 迁移到 ConcurrentHashMap |
选型口诀:无排序无顺序用 HashMap,需要顺序用 Linked,需要排序用 Tree,并发场景用 Concurrent。
