应充分预热JVM、多次采样、排除GC等干扰,用System.nanoTime()批量测量百万次contains(),分别统计命中/未命中平均值与99分位耗时,并用JMH或对比ArrayList/TreeSet验证常数级性能。

直接用 System.nanoTime() 测量单次 contains() 耗时意义不大,因为 JVM 预热、JIT 编译、GC 干扰等因素会让结果严重失真。要测出 HashSet 真实的查找性能,关键在于:**预热充分、多次采样、排除干扰、关注平均与分布**。
预热集合和 JVM
HashSet 查找快慢受底层哈希表结构(如桶数量、是否触发扩容)和 JIT 优化程度影响。必须先让 JVM 运行足够多次,触发热点代码编译:
- 构造一个中等规模(如 10 万)的 HashSet,并插入随机或典型数据
- 在正式计时前,反复调用
contains()至少数千次(例如 10,000 次),不记录耗时 - 可配合
-XX:+PrintCompilation观察方法是否已被编译
批量测量 + 统计分析
单次调用可能低至纳秒级,且易受系统噪声干扰。应测量大量查询(如 100 万次),再计算平均值、中位数、99 分位耗时:
- 用
System.nanoTime()记录起止时间,避免currentTimeMillis()的低精度问题 - 混合测试“命中”(存在元素)和“未命中”(不存在元素),两者行为不同:命中取决于哈希定位+equals;未命中通常只需一次哈希+桶空检查,但最坏情况也要遍历链表/红黑树
- 建议分别统计:存在元素的平均耗时、不存在元素的平均耗时、以及长尾(如 99% 分位)耗时
控制变量,避免常见干扰
很多看似微小的操作会污染结果:
立即学习“Java免费学习笔记(深入)”;
- 不要在循环内创建新对象(如每次 new String)、避免触发 GC;查询 key 应复用已有引用
- 关闭日志输出、IDE 调试器、其他高负载程序;可在命令行用
-Xmx2g -Xms2g固定堆大小,减少 GC 波动 - 使用 JMH(Java Microbenchmark Harness)是更可靠的选择——它自动处理预热、Fork JVM、统计校准等,比手写循环严谨得多
对比基准,理解结果含义
单纯看“平均 8ns”没意义,要结合场景判断是否合理:
- 对比
ArrayList.contains()(O(n))和TreeSet.contains()(O(log n)),确认 HashSet 确实呈常数级增长 - 改变 HashSet 初始容量(如
new HashSet(1000)vsnew HashSet(100000)),观察扩容对后续查找的影响 - 故意用低质量 hashcode(如所有对象返回 1)制造哈希碰撞,验证退化为链表遍历时的性能下降



















