Random:伪随机数生成器
本章定位:
java.util.Random是 Java 标准的伪随机数生成器,基于线性同余算法。理解种子的作用、取值范围边界和高并发替代方案,是写出健壮随机逻辑的前提。
黄俪的抽奖系统
飞翔科技年会临近,黄俪被分配开发员工抽奖系统。她很快写出了第一版代码——但测试时发现每次运行抽奖结果完全一样。
Random r = new Random(42); // 固定种子 = 固定序列
// 测试时总是 "小明" 中一等奖,"小明" 表示压力很大
白歌看了一眼:"种子 42 是调试用的,上线记得用无参构造。不过在那之前,先搞清楚 Random 的底层原理。"
Random 类详解
构造方法
| 构造方法 | 说明 |
|---|---|
new Random() | 使用当前时间纳秒作为种子(JDK 8 实际使用 System.nanoTime() 与一个原子计数器异或) |
new Random(long seed) | 使用指定种子,产生完全可预测的随机序列 |
常用方法
| 方法 | 返回范围 | 说明 |
|---|---|---|
nextInt() | [-2³¹, 2³¹-1] | 随机 int(全范围) |
nextInt(int bound) | [0, bound) | 0 到 bound-1 的随机整数 |
nextDouble() | [0.0, 1.0) | 随机 double |
nextBoolean() | true / false | 随机布尔值 |
nextLong() | 全范围 | 随机 long |
nextFloat() | [0.0, 1.0) | 随机 float |
nextGaussian() | 均值0、标准差1 | 正态分布(高斯)随机数 |
种子的作用
相同种子 == 相同随机序列。这在游戏开发(地图种子)和测试(可复现的随机数据)中非常重要。
Random r1 = new Random(42);
Random r2 = new Random(42);
// r1 和 r2 生成的随机序列完全一样!
System.out.println(r1.nextInt(100)); // 假设输出 88
System.out.println(r2.nextInt(100)); // 也是 88
Random使用的是伪随机数生成器(PRNG),基于线性同余公式。给定相同种子,生成的随机序列完全确定。这不是真正的随机(真随机需要物理熵源),但对于绝大多数应用场景"够用且可复现"。
完整示例一:员工抽奖系统
场景
飞翔科技年会上需要从全体员工中随机抽取幸运奖。
代码
import java.util.Random;
public class LuckyDraw {
public static void main(String[] args) {
// 飞翔科技员工名单
String[] employees = {
"大翔", "白歌", "小崔", "小明", "小红",
"李工", "张工", "王工", "赵工", "钱工"
};
Random random = new Random(); // 无参构造,使用时间种子
System.out.println("===== 飞翔科技年会抽奖 =====\n");
// ----- 1. 三等奖:3 名,每人 500 元 -----
System.out.println("--- 三等奖(红包 500 元) ---");
for (int i = 0; i < 3; i++) {
int index = random.nextInt(employees.length); // [0, 10)
System.out.println(" 恭喜 " + employees[index] + " 获得三等奖!");
}
// ----- 2. 二等奖:2 名,生成 4 位验证码 -----
System.out.println("\n--- 二等奖(带验证码) ---");
for (int i = 0; i < 2; i++) {
int index = random.nextInt(employees.length);
int code = 1000 + random.nextInt(9000); // [1000, 9999]
System.out.println(" 恭喜 " + employees[index]
+ " 获得二等奖!兑奖码: " + code);
}
// ----- 3. 一等奖:1名,随机金额红包 -----
System.out.println("\n--- 一等奖 ---");
int winnerIndex = random.nextInt(employees.length);
// 随机金额 5000 ~ 10000 元
double amount = 5000 + random.nextDouble() * 5000;
System.out.println(" 恭喜 " + employees[winnerIndex]
+ " 获得一等奖!奖金: " + String.format("%,.2f", amount) + " 元");
// ----- 4. 生成可复现的抽奖(固定种子) -----
System.out.println("\n--- 测试模式(种子=42) ---");
Random testRandom = new Random(42);
for (int i = 0; i < 3; i++) {
System.out.println(" 第" + (i+1) + "次: 员工索引 "
+ testRandom.nextInt(employees.length));
}
// 每次运行结果相同!
}
}
运行输出(无参构造部分每次不同;种子=42 部分始终相同)
===== 飞翔科技年会抽奖 =====
--- 三等奖(红包 500 元) ---
恭喜 小明 获得三等奖!
恭喜 大翔 获得三等奖!
恭喜 王工 获得三等奖!
--- 二等奖(带验证码) ---
恭喜 李工 获得二等奖!兑奖码: 5823
恭喜 小崔 获得二等奖!兑奖码: 3019
--- 一等奖 ---
恭喜 白歌 获得一等奖!奖金: 7,345.62 元
--- 测试模式(种子=42) ---
第1次: 员工索引 8
第2次: 员工索引 7
第3次: 员工索引 6
完整示例二:高并发随机与密码学安全随机
场景
孔蓝在做飞翔科技的自动化测试时,发现大量并发测试用例使用 Math.random() 导致性能瓶颈。她同时需要生成安全的临时密码——这需要 SecureRandom 而非普通 Random。
代码
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.security.SecureRandom;
public class RandomAdvancedDemo {
public static void main(String[] args) {
System.out.println("===== 飞翔科技 随机数进阶应用 =====\n");
// ========== 1. nextInt(bound) 通用公式 ==========
System.out.println("--- 生成指定范围随机整数 ---");
Random r = new Random();
// 公式: random.nextInt(max - min + 1) + min
System.out.print("[1, 6] 骰子 10 次: ");
for (int i = 0; i < 10; i++) {
System.out.print((r.nextInt(6) + 1) + " ");
}
System.out.println();
System.out.print("[10, 20] 随机 10 次: ");
for (int i = 0; i < 10; i++) {
System.out.print((r.nextInt(11) + 10) + " ");
}
System.out.println();
// ========== 2. ThreadLocalRandom:高并发无锁随机 ==========
System.out.println("\n--- ThreadLocalRandom 高并发随机 ---");
// 每个线程独立的 Random 实例,无线程竞争
ThreadLocalRandom tlr = ThreadLocalRandom.current();
System.out.println("当前线程随机 int [0, 100): " + tlr.nextInt(100));
System.out.println("当前线程随机 double [0, 1): " +
String.format("%.4f", tlr.nextDouble()));
System.out.println("当前线程随机 long: " + tlr.nextLong());
System.out.println("当前线程随机 boolean: " + tlr.nextBoolean());
// 生成 [1000, 9999] 范围内的验证码
int verifyCode = tlr.nextInt(1000, 10000); // JDK 17 新 API,这里用传统方式
verifyCode = 1000 + tlr.nextInt(9000);
System.out.println("4 位验证码: " + verifyCode);
// ========== 3. SecureRandom:密码学安全随机 ==========
System.out.println("\n--- SecureRandom 安全随机 ---");
SecureRandom secureRandom = new SecureRandom();
// 生成安全临时密码(8 位,大小写字母 + 数字)
String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
StringBuilder password = new StringBuilder();
for (int i = 0; i < 8; i++) {
password.append(chars.charAt(secureRandom.nextInt(chars.length())));
}
System.out.println("安全临时密码: " + password);
// 生成密码学安全的随机令牌(16 字节 = 32 个十六进制字符)
byte[] tokenBytes = new byte[16];
secureRandom.nextBytes(tokenBytes);
StringBuilder token = new StringBuilder();
for (byte b : tokenBytes) {
token.append(String.format("%02x", b));
}
System.out.println("安全令牌 (32 hex): " + token);
// ========== 4. nextGaussian():正态分布随机 ==========
System.out.println("\n--- nextGaussian() 正态分布采样 ---");
Random gRandom = new Random(42);
System.out.println("均值 0、标准差 1 的正态分布 10 次采样:");
for (int i = 0; i < 10; i++) {
System.out.printf(" %.4f%n", gRandom.nextGaussian());
}
// 模拟考试成绩:均值 70、标准差 10
System.out.println("\n模拟考试成绩(均值 70、标准差 10):");
Random scoreRandom = new Random();
for (int i = 0; i < 5; i++) {
double score = 70 + scoreRandom.nextGaussian() * 10;
// 限制在 [0, 100]
score = Math.max(0, Math.min(100, score));
System.out.printf(" 考生 %d: %.1f 分%n", i + 1, score);
}
}
}
运行输出
===== 飞翔科技 随机数进阶应用 =====
--- 生成指定范围随机整数 ---
[1, 6] 骰子 10 次: 4 6 1 3 5 2 6 4 1 3
[10, 20] 随机 10 次: 17 12 19 10 15 20 11 13 18 14
--- ThreadLocalRandom 高并发随机 ---
当前线程随机 int [0, 100): 73
当前线程随机 double [0, 1): 0.2847
当前线程随机 long: -8234019283746501923
当前线程随机 boolean: true
4 位验证码: 5812
--- SecureRandom 安全随机 ---
安全临时密码: Kx9mP2qR
安全令牌 (32 hex): a3f7b2c10e4d5698f1a2b3c4d5e6f708
--- nextGaussian() 正态分布采样 ---
均值 0、标准差 1 的正态分布 10 次采样:
-0.7829
1.2453
-0.3912
0.5678
2.1045
-1.8902
0.3456
-0.1234
0.8765
-0.5432
模拟考试成绩(均值 70、标准差 10):
考生 1: 68.3 分
考生 2: 82.1 分
考生 3: 73.5 分
考生 4: 58.7 分
考生 5: 71.2 分
易错场景
反例一:生成指定范围随机数的错误方式
// ❌ 错误:nextInt(bound) 返回 [0, bound),不含 bound
int dice = random.nextInt(6); // 输出 0~5,不是 1~6!
纠正:
// ✅ 正确:生成 [1, 6] 的骰子点数
int dice = random.nextInt(6) + 1; // [0,5] + 1 = [1,6]
// 通用公式:[min, max] 范围内的随机整数
// int result = random.nextInt(max - min + 1) + min;
int between10and20 = random.nextInt(11) + 10; // [0,10] + 10 = [10,20]
反例二:Math.random() 在并发场景下的问题
// ❌ Math.random() 底层是全局单例 Random,高并发下有锁竞争
for (int i = 0; i < 1000000; i++) {
double r = Math.random(); // 底层 synchronized
}
纠正:
// ✅ 高并发场景使用 ThreadLocalRandom(JDK 7+)
import java.util.concurrent.ThreadLocalRandom;
for (int i = 0; i < 1000000; i++) {
int r = ThreadLocalRandom.current().nextInt(100); // 无锁
}
反例三:用 Random 生成安全令牌
// ❌ 错误:普通 Random 的种子可被预测,不适合安全场景
Random r = new Random();
byte[] token = new byte[16];
for (int i = 0; i < 16; i++) {
token[i] = (byte) r.nextInt(256); // 不安全!可被攻击者预测
}
纠正:
// ✅ 正确:使用 SecureRandom 生成密码学安全随机数
SecureRandom sr = new SecureRandom();
byte[] token = new byte[16];
sr.nextBytes(token); // 使用系统熵源,不可预测
面试考点
Q1:Math.random() 和 new Random().nextDouble() 有什么区别?
没有本质区别——
Math.random()底层就是Random.nextDouble()。区别在于Math.random()使用的是全局单例Random对象,无法指定种子;而new Random(long seed)可以指定种子以获得可复现的随机序列。
Q2:如何生成 [1, 100] 的随机整数?
new Random().nextInt(100) + 1。nextInt(bound)返回[0, bound),加 1 后得[1, 101),即[1, 100]。通用公式:[min, max]→random.nextInt(max - min + 1) + min。
Q3:Random 的"伪随机"是什么意思?
Random 使用的是伪随机数生成器(PRNG),基于线性同余公式
seed = (seed * multiplier + addend) mod 2^48。给定相同种子,生成的随机序列完全确定。这不是真正的随机(真随机需要物理熵源),但对于绝大多数应用场景"够用且可复现"。需要密码学级别的随机应使用SecureRandom。
Q4:为什么高并发场景不推荐 Math.random()?
Math.random()底层调用java.util.Random,其next()方法使用 CAS 操作保证线程安全,高并发下多个线程竞争同一个AtomicLong seed,性能下降明显。JDK 7 引入的ThreadLocalRandom为每个线程维护独立的种子,无竞争,性能更高。
Q5:ThreadLocalRandom 和 SecureRandom 分别用于什么场景?
ThreadLocalRandom用于高并发场景下的非安全随机需求(如游戏、模拟、A/B 测试),特点是高性能、无锁。SecureRandom用于密码学安全场景(生成密钥、令牌、盐值),特点是不可预测、使用系统熵源,但性能较低。两者不可互换——不要用 Random 做安全,也不要用 SecureRandom 做高频随机。
白歌对黄俪说:"记住三条铁律——范围公式
nextInt(max-min+1)+min、高并发用ThreadLocalRandom、安全场景用SecureRandom。掌握了这三条,Random 就不会给你带来惊喜了。"