12-集合避坑 12 条:性能调优与那些年踩过的雷

发布时间:2026/10/22 17:31:28
12-集合避坑 12 条:性能调优与那些年踩过的雷 12 条避坑 上线 checklist拦掉多数事故把生产环境最高频的 12 个坑和对应的调优手段集中摆出来。很多性能问题和诡异 Bug根子都在集合使用习惯上——容量设错、hashCode 写错、遍历时删除、并行流乱用……每一条都配代码和后果照着排查能省无数加班。坑一不预设容量反复扩容ArrayList、HashMap默认容量小10 / 16批量数据会一路扩容 拷贝。// 反例循环 add 十万条触发几十次 1.5 倍扩容ListUserlistnewArrayList();for(Useru:users)list.add(u);// 正解预估容量一次到位ListUserlistnewArrayList(users.size());// 或 HashMap(int)(expectedSize / 0.75f 1)MapString,UsermapnewHashMap((int)(users.size()/0.75f1));扩容对 ArrayList 是Arrays.copyOfO(n) 整体搬对 HashMap 是重建桶 高低位拆链。大数据量下预设容量性能可差数倍。坑二hashCode/equals 契约破坏去重和查找失效放进 HashSet/HashMap 的 key 的对象必须正确实现equals和hashCode且参与哈希的字段不可变。classUser{Stringid;// 必须同时重写 equals 和 hashCode且只用不可变字段Overridepublicbooleanequals(Objecto){...}OverridepublicinthashCode(){returnObjects.hash(id);}}只重写其一、或改了参与 hashCode 的字段对象就从集合里消失——contains/get找不到、remove删不掉成了游离垃圾。这是最隐蔽的 Bug 之一不报错但逻辑悄悄错。坑三遍历时删除ConcurrentModificationException// 反例for-each 里直接 remove → 抛 CMEfail-fastfor(Strings:list){if(s.isEmpty())list.remove(s);}// 正解 1用迭代器自己的 removeIteratorStringitlist.iterator();while(it.hasNext()){if(it.next().isEmpty())it.remove();}// 正解 2Java 8 removeIflist.removeIf(String::isEmpty);// 正解 3先收集再删并发集合或需复杂判断时list.removeAll(list.stream().filter(String::isEmpty).collect(toList()));fail-fast机制靠modCount和迭代器的expectedModCount比对你在迭代器外部改了结构下次next()立刻抛ConcurrentModificationException。并发容器是弱一致不抛但语义不同别混。坑四subList 是视图改原列表会炸ListIntegersublist.subList(0,3);sub.set(0,999);// 原 list 第 0 位也变list.add(100);// 改了原列表结构sub.get(0);// 抛 ConcurrentModificationException要独立副本new ArrayList(list.subList(0, 3))。Map 的keySet()/values()/entrySet()同样返回视图结构改动后视图行为未定义要快照就new HashSet(map.keySet())。坑五Arrays.asList 返回定长 Listadd 抛异常ListStringlArrays.asList(a,b);l.add(c);// UnsupportedOperationException// 要可变ListStringl2newArrayList(Arrays.asList(a,b));同理List.of返回的是不可变且不允许 null也别 add。需要可变集合永远从new ArrayList/new HashMap起手。坑六选错结构性能腰斩随机访问多却用 LinkedList →get(i)O(n)整段 O(n²)。查重/映射却用 List contains→ O(n)应换 HashSet/HashMap 的 O(1)。频繁中间插入却用 ArrayList → 整体搬移 O(n)应换 LinkedList持迭代器或别的結構。需要排序范围查询却用 HashMap → 应换 TreeMap/ConcurrentSkipListMap。选型口诀随机读 ArrayList去重查重 HashMap/HashSet两端操作 ArrayDeque排序范围 TreeMap并发 CHM/COW/跳表。坑七并行流 parallelStream 乱用// 反例共享可变集合 并行 → 数据竞争、结果错ListIntegeroutnewArrayList();list.parallelStream().forEach(out::add);// 非线程安全丢元素// 正解用收集器或并发集合ListIntegerout2list.parallelStream().collect(Collectors.toList());parallelStream不是银弹① 收集到非线程安全集合会丢数据② 小数据量、有 IO/锁 时并行反而更慢线程调度开销③ 共用 ForkJoinPool一个慢任务拖垮全局。要并行就把结果交给collect或ConcurrentHashMap别手填普通集合。坑八把 ConcurrentHashMap 当精确计数器 / 缓存// size() 弱一致不能当精确计数if(map.size()0){...}// 并发下不可靠// 高并发计数用 LongAdderLongAddercounternewLongAdder();counter.increment();longccounter.sum();// 精确、高争用下远快于 AtomicLongCHM 不提供过期淘汰做缓存要自己加 TTLDelayQueue 清理或换 Caffeine/Guava Cache。计数请用LongAdder别用AtomicLong在高争用下硬扛。坑九Integer 自动装箱的隐性开销与缓存陷阱// 大循环里反复装箱产生大量临时 Integer 对象ListIntegerlistnewArrayList();for(inti0;i1000000;i)list.add(i);// 每次 new Integer或取缓存百万次 add 产生百万个 Integer 对象GC 压力大。热点循环尽量用int[]/TIntArrayList第三方原始类型集合避免装箱。另注意Integer缓存 -128~127范围外new Integer(200) ! new Integer(200)HashMap用 Integer 作 key 时按 equals 比不受影响但若用比较会踩坑。坑十equals 比较用 而非 equals集合的contains/remove(Object)/indexOf全靠equals。若你用比较对象尤其包装类型、自定义类会因不是同一个引用而判断失败ListIntegerlistList.of(200,300);list.contains(200);// trueequalsIntegera200,b200;ab;// false范围外不缓存是两个对象一律用equals或集合自带的 contains别用比对集合元素。坑十一HashMap 初始容量算错触发中途扩容// 想装 1000 个元素直接 new HashMap(1000) 不够// 阈值 1000 * 0.75 750第 751 个就扩容// 正确/(0.75)1newHashMap((int)(1000/0.75f1));// ≈ 1334向上取 2 的幂 2048全程不扩容不校准则在填充中途扩一次白拷贝一次。批量灌数据前务必按(size/0.751)算初始容量。坑十二迭代器遍历大集合做重活阻塞/超时// 反例在 for-each 里同步调远程、写 DB单线程串行进, 慢且易超时for(Ordero:bigList){rpc.call(o);}// 正解分批 并行 超时或推到消息队列ListListOrderbatchespartition(bigList,100);batches.parallelStream().forEach(b-processBatch(b));大集合遍历里塞同步重 IO会把处理时间线性拉长。要么分批异步、要么丢给线程池/队列别在主线程一条龙扫完。调优检查清单上线前过一遍项检查容量预估 size 的 ArrayList/HashMap 是否预设容量HashMap 是否/(0.751)哈希作 key 的对象是否重写了 equalshashCode参与字段是否不可变遍历删除是否用迭代器/removeIf有无 for-each 里 remove视图陷阱subList / keySet / entrySet 是否误当独立集合结构选型随机读/去重/排序/并发 是否选对并行parallelStream 是否收集到线程安全容器是否有共享可变状态计数高并发计数是否用 LongAdder 而非 size()/AtomicLong装箱热点循环是否避免 Integer 装箱比较集合元素比较是否用 equals 而非 大遍历是否避免在主线程同步扫大集合做重 IO面试追问Qfail-fast 和 fail-safe 区别fail-fastHashMap/ArrayList检测到结构变更modCount 不一致立刻抛 CME迭代中不允许并发改fail-safeCHM、COW 的迭代器基于快照/弱一致不抛但可能看不到最新变更。Q为什么推荐 LongAdder 做高并发计数AtomicLong 所有线程 CAS 同一个变量高争用下大量自旋重试LongAdder 把计数分散到多个 Cell争用低时合并吞吐高得多。QHashMap 预设容量公式为什么是 /0.751要让填充中途不触发resize阈值cap*0.75 ≥ size故cap ≥ size/0.751 留余量再向上取 2 的幂。总结集合避坑 12 条① 预设容量HashMap 用 size/0.751② 正确实现不可变 key 的 equals/hashCode③ 遍历删除用迭代器/removeIf④ 别把 subList/keySet 当独立集合⑤ 按随机读/去重/排序/并发选对结构⑥ 并行流收集到线程安全容器⑦ 高并发计数用 LongAdder⑧ 避免 Integer 装箱与 比较⑨ CHM 的 size 弱一致、无 TTL⑩ 大遍历别塞同步重 IO。把这 12 条当上线 checklist能拦掉绝大多数集合相关的性能事故和诡异 Bug。整个集合框架系列到此收口——从总览到源码到并发到调优你已拿到完整地图。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询