Java集合框架中的Set与Map的应用场景

该用Set而非List当需自动去重且不关心顺序或索引;HashSet平均O(1)、LinkedHashS

et保插入序、TreeSet支持排序但O(log n),注意null限制。

什么时候该用 Set 而不是 List

当你需要自动去重、且不关心元素顺序或索引访问时,Set 是更合适的选择。比如读取一批用户 ID 并统计唯一数量,用 List 后手动去重不仅慢(O(n²)),还容易漏掉边界情况。

  • HashSet 最常用:底层是哈希表,插入/查找平均 O(1),但不保证顺序
  • LinkedHashSet 适合需要按插入顺序遍历的场景,比如最近访问的资源缓存
  • TreeSet 用于需要自然排序或自定义排序的集合,比如排行榜分数去重后升序展示,但增删查是 O(log n)

注意:Set 不能存 nullTreeSet 直接抛 NullPointerExceptionHashSet 允许一个 null,但要小心后续比较逻辑)

Map 的键为什么必须是不可变对象

因为 Map(尤其是 HashMap)依赖键的 hashCode()equals() 定位和比较元素。如果键对象在放入后被修改,导致 hashCode() 变化,就再也找不到它了——不是丢失,是“藏起来了”。

  • 常见踩坑:用可变的 StringBuilder 或自定义类作键,没重写 hashCode()equals(),或重写了但字段参与计算后又被改
  • 安全做法:优先用 StringInteger 等不可变类型;若必须用自定义类,确保所有参与 hashCode()/equals() 的字段在构造后不可变

例如:

Map userScore = new HashMap<>();
userScore.put("alice", 95);
// 后续用 "alice" 能稳定取到,因为 String 不可变

SetMap 在实际业务中怎么配合用

很多场景下二者不是二选一,而是协同:用 Map 存主数据 + 关联关系,用 Set 做快速存在性判断或去重中间结果。

  • 权限校验:把用户拥有的角色 ID 存进 HashSet,检查是否包含某个角色时用 roles.contains("ADMIN"),比遍历 List 快得多
  • 批量更新防重:从消息队列收到一批订单 ID,先塞进 Set 去重,再查 Map 批量加载已有订单,避免重复 DB 查询
  • 配置白名单:把合法的 API 路径存在 Set,拦截器里直接判断 if (!allowedPaths.contains(path)) reject();

性能关键点:别为了“看起来统一”而强行把 MapSet 用——语义不清,内存浪费,还可能因 null 值引发误判。

并发环境下选哪个实现

单线程用 HashSet/HashMap 没问题,但多线程写入必须换线程安全版本,否则会出错或死循环(JDK 7 的 HashMap 扩容时可能形成环形链表)。

  • 简单读多写少:用 Collections.synchronizedSet() / synchronizedMap(),但锁粒度大,吞吐低
  • 高并发读写:优先选 ConcurrentHashMap;它没有 ConcurrentHashSet,但可用 ConcurrentHashMap.newKeySet()(JDK 8+)替代
  • 注意:ConcurrentHashMapsize() 不是精确值(为避免锁竞争而妥协),需精确计数建议用 LongAdder 单独维护

最容易被忽略的是:即使用了 ConcurrentHashMap,复合操作如“检查不存在再 put”仍需 computeIfAbsent()putIfAbsent(),否则仍有竞态。