collections.shuffle 默认使用 new random()(系统时间种子的伪随机),易导致同一毫秒内序列重复;应显式传入 securerandom 或自定义种子的 random 以提升随机性与可重现性。

为什么 Collections.shuffle 默认不“真随机”
它用的是 new Random(),也就是基于系统时间种子的伪随机——同一毫秒内初始化的多个 Random 实例会产生完全相同的打乱序列。这在单元测试、多线程预生成场景下极易复现 bug。
常见错误现象:Collections.shuffle(list) 在循环中反复调用,结果每次输出顺序都一样;或者不同 JVM 进程里跑出相同乱序结果。
- 真正需要不可预测性时,显式传入
new SecureRandom() - 若只是避免“每次启动都一样”,可提前用
System.nanoTime()做种子:new Random(System.nanoTime() ^ System.currentTimeMillis()) -
SecureRandom有性能开销,高吞吐批量打乱(如每秒千次)慎用
Collections.shuffle 的底层交换逻辑怎么走
它用的是 Fisher-Yates(Knuth Shuffle)算法的原地变体:从后往前遍历,每次随机选一个索引 ≤ 当前位置的元素来交换。不会产生偏差,但前提是 Random.nextInt(int) 均匀。
关键点在于:它只依赖 Random.nextInt(bound) 的均匀性。而 JDK 17+ 中 Random 的 nextInt 已修复了旧版低位周期短的问题,但 SecureRandom.nextInt() 仍可能因实现差异略慢。
立即学习“Java免费学习笔记(深入)”;
- 不支持自定义比较器或排序逻辑——它只做位置置换,不管元素内容
- 对
ArrayList是 O(n) 时间 + O(1) 额外空间;对LinkedList会退化成 O(n²),因为get(i)是遍历 - 传入不可变集合(如
Arrays.asList()返回的)会抛UnsupportedOperationException,因为 shuffle 需要 set 操作
想控制随机源,但又不想改原有调用链
直接替换全局 Random 不可行,Collections.shuffle 内部 new 的是局部实例。必须显式传参。
最轻量的兼容做法:封装一层工具方法,保留老接口语义但注入可控随机源。
public static <T> void shuffle(List<T> list, Random rnd) {
if (rnd == null) rnd = new Random();
Collections.shuffle(list, rnd);
}
- 避免用
ThreadLocal<random></random>包装默认调用——容易和业务中其他Random混淆,且没解决种子问题 - 如果已有大量
Collections.shuffle(list)调用,建议 grep + 替换为工具方法,别硬 patchRandom构造逻辑 - 注意:
Collections.shuffle(list, rnd)中的rnd是强引用,别传生命周期短于 list 的临时对象
乱序结果还要可重现?那就得管住种子
可重现 ≠ 用固定种子就完事。JDK 版本升级可能让同一 Random(long seed) 在不同版本产出不同序列(虽不常见,但 Random 实现细节未严格规定)。
真正稳定的方案只有两种:自己实现 Fisher-Yates + 固定种子的 LCG,或锁定 JDK 版本 + 显式种子。
- 不要依赖
new Random(123)在 JDK 8/11/17 上结果一致——文档没保证 - 如果必须跨环境一致,用
new Random(123L)+ 注释清楚 JDK 版本,比靠运气强 - 测试中用固定种子时,记得每个 test 方法单独 new
Random,别共用实例,否则顺序串扰










