# 问题

30. 在实际业务开发中,如何选择合适的 Map 实现?举例说明你的选择依据。

# 标准答案

选择合适的 Map 需要综合考虑查询性能、插入性能、并发安全、排序需求、内存管理等因素。不同 Map 适用于不同业务场景,例如:

  • HashMap(O(1) 查询):适用于高效的 KV 存储,如用户会话缓存。
  • ConcurrentHashMap(高并发优化):适用于多线程环境,如请求去重。
  • TreeMap(O(log N) 查询 + 有序):适用于范围查询,如订单按时间排序。
  • LinkedHashMap(LRU 访问顺序):适用于最近访问缓存,如本地 LRU 缓存。
  • WeakHashMap(自动回收):适用于存储短生命周期对象,如临时缓存。
  • IdentityHashMap(按引用比较):适用于对象池、序列化优化,如对象去重。
  • ConcurrentSkipListMap(线程安全的 TreeMap):适用于有序并发访问,如时间序列数据。

# 答案解析

# 1. 缓存用户 Token(高效 KV 查询)

需求:用户登录后,服务端缓存用户 Token,每次请求验证 Token 是否有效。
选择ConcurrentHashMap(保证线程安全,O(1) 查询)。

private static final Map<String, String> tokenCache = new ConcurrentHashMap<>();
tokenCache.put("user123", "token_xyz");
String token = tokenCache.get("user123"); // O(1) 查询
1
2
3

为什么不用 HashMap?
HashMap 线程不安全,可能导致数据竞争或扩容时的竞态条件。

# 2. 记录最近访问的 100 个页面(LRU 缓存)

需求:维护最近访问的页面,超过 100 个时删除最旧的数据。
选择LinkedHashMap(按照访问顺序维护元素,自动移除旧数据)。

LinkedHashMap<String, String> pageCache = new LinkedHashMap<>(100, 0.75f, true) {
    protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
        return size() > 100; // 超过 100 个元素时移除最老的数据
    }
};
pageCache.put("page1", "Home");
pageCache.put("page2", "Profile");
1
2
3
4
5
6
7

为什么不用 HashMap?
HashMap 不能维护访问顺序,无法实现 LRU 逻辑。

# 3. 电商订单按时间排序(范围查询)

需求:按下单时间排序,快速查询某时间段的订单。
选择TreeMap(支持 O(log N) 查询 和子区间查询)。

TreeMap<Long, String> orderMap = new TreeMap<>();
orderMap.put(1700000000000L, "Order1");
orderMap.put(1700000002000L, "Order2");
NavigableMap<Long, String> subMap = orderMap.subMap(1700000000000L, true, 1700000002000L, true);
1
2
3
4

为什么不用 HashMap?
HashMap 无法保持 key 的有序性,不适用于范围查询。

# 4. 数据库索引(并发排序查询)

需求:需要在多线程环境下高效范围查询,如分布式日志查询。
选择ConcurrentSkipListMap(线程安全的 TreeMap)。

ConcurrentSkipListMap<Long, String> logMap = new ConcurrentSkipListMap<>();
logMap.put(1700000000000L, "Log1");
logMap.put(1700000003000L, "Log2");
NavigableMap<Long, String> subMap = logMap.subMap(1700000000000L, true, 1700000003000L, true);
1
2
3
4

为什么不用 TreeMap?
TreeMap 线程不安全,在多线程环境下可能导致数据不一致或并发异常。

# 5. 短生命周期对象缓存(避免内存泄漏)

需求:存储弱引用对象,当对象没有强引用时自动回收。
选择WeakHashMap(key 被 GC 回收时,自动删除对应的 entry)。

WeakHashMap<Object, String> weakMap = new WeakHashMap<>();
Object key = new Object();
weakMap.put(key, "Temporary Data");
key = null; // 让 key 变成弱引用,GC 后 weakMap 自动删除该 entry
System.gc(); // 触发 GC,weakMap 可能变空
1
2
3
4
5

为什么不用 HashMap?
HashMap 强引用 key,即使 key 不再使用,仍然占用内存,可能导致内存泄漏。

# 深入追问

  1. 为什么 ConcurrentHashMapCollections.synchronizedMap(new HashMap<>()) 更高效?

    • ConcurrentHashMap 采用 CAS + 分段锁,多个线程可以同时操作不同的 bucket。
    • synchronizedMap 锁整个 Map,并发性能低。
  2. 为什么 LinkedHashMap 适合 LRU 缓存,而 TreeMap 不适合?

    • LinkedHashMap 维护访问顺序,每次访问会调整顺序,淘汰最久未使用的数据。
    • TreeMap 只按 key 排序,不支持访问顺序调整。
  3. 为什么 WeakHashMap 适合存储临时对象,而 HashMap 可能导致内存泄漏?

    • WeakHashMap 的 key 是弱引用,GC 之后会自动移除 entry。
    • HashMap 强引用 key,即使不再使用,仍然占用内存。

# 相关面试题

  • 如何选择适合的 Map?不同 Map 适用于哪些场景?
  • ConcurrentHashMap 为什么比 synchronizedMap 更高效?
  • LRU 缓存为什么用 LinkedHashMap 而不是 TreeMap?
  • WeakHashMap 适用于哪些场景?它的 key 为什么是弱引用?