.## 1. 本文你将学到的内容
当你在 Java 中尝试使用“随机数”时,你会很快遇到多种选项,例如 Math.random()、Random 和 SecureRandom。
很多人最终会想:“我该用哪一个?”
在本节中,我们将直接给出结论,并阐明阅读本文至结束后你能够做到的事情。先把大局弄清楚,再深入细节和代码,后面的章节就会容易得多。
1.1 你将了解在 Java 中生成随机数的主要方式
本文将 一步一步 解释在 Java 中生成随机数的主要方式。
具体来说,你将学到:
- 一个可以立刻使用的简单方法
- 一个能够提供更强程序化控制的方法
- 一个在安全性重要时使用的方法
我们会 按使用场景 对所有内容进行组织,方便你选择合适的方案。
这意味着本文适用于以下两类读者:
- 想要尝试示例代码的初学者
- 想要超越“能跑就行”思维的进阶者
结构设计上兼顾了这两类读者的需求。
1.2 你将了解范围规则及常见误解
在使用随机数时,范围选择 是最大的绊脚石之一。
例如:
- 需要一个 0 到 9 之间的随机数
- 需要模拟掷骰子,得到 1 到 6 之间的随机数
- 需要包含负数的随机数
在这些情况下,常会出现以下问题:
- 上界是否包含?
- 下界是否始终包含?
- 为什么没有得到预期的数值?
本文将以初学者友好的流程解释:
“为什么会这样?” → “如何正确写?”
1.3 你将了解可复现性与风险之间的区别
随机数在不同情境下可能有 相反的需求:
- 需要每次都得到不同的结果
- 需要多次复现相同的结果
例如:
- 在测试和调试时,你希望复现“相同的随机值”
- 在密码和令牌生成时,你需要“不可预测的随机值”
如果在不了解这一区别的情况下使用随机数,可能会导致:
- 测试不稳定
- 安全隐患的实现
本文会明确区分:
“应当可复现的随机性” 与 “绝不能复现的随机性”
1.4 你将能够在“安全随机” 与 “危险随机” 之间做出选择
Java 提供了多种看似相似的随机数实现,但它们的 用途完全不同:
- 适用于游戏和示例代码的随机性
- 适用于业务应用的随机性
- 绝对必须用于安全场景的随机性
如果不区分用途直接使用,往往会导致:
- “看起来能跑,但实际上很危险”
- “代码在后期会成为问题”
本文将以 “针对该使用场景,使用此实现” 的形式,给出决策依据和理由。
1.5 即使是初学者也能解释“为什么这样做是正确的”
我们不只是列出代码示例,而是关注背后的思考过程,例如:
- 为什么要使用这个方法
- 为什么这种写法是正确的
- 为什么其他方式不合适
我们强调 背景和思维方式。
因此,这对以下读者非常有帮助:
- 想要理解并实际使用(而不是死记硬背)的人
- 想要达到能够向他人解释的水平的人
2. Java 中“随机数”的含义
在生成 Java 随机数之前,本节先梳理 你必须了解的核心基础。
如果直接跳过只复制代码,后面几乎必然会产生困惑。
2.1 “Random” 并不等同于 “完美随机”
一个常见的初学者误解是:
在 Java 中生成的随机数并非“完美随机”。
大多数在 Java 中使用的随机值更准确的描述是:
- 根据固定规则(算法)计算的数字
- 看起来随机,但内部遵循某种模式
这种随机性被称为 伪随机数(PRNG 输出)。
2.2 什么是伪随机数生成器(PRNG)?
伪随机数生成器是一种机制,它:
- 从一个初始值(种子)开始
- 重复进行数学计算
- 产生看似随机的序列
主要优点包括:
- 生成速度快
- 相同的种子会产生相同的随机序列
- 在计算机上易于处理
另一方面,缺点有:
- 如果算法已知,则可预测
- 可能不适用于安全场景
2.3 “可复现随机性”何时有用
乍一看,你可能会认为:
如果相同的随机值出现,这不是不随机吗?因此会很糟糕?
但实际上,可复现的随机性极其重要。
例如:
- 你想在测试代码中每次都验证相同的结果
- 你想重现一次错误报告
- 你想比较仿真结果
在这些情况下,拥有以下特性要更实用:
- 每次运行都使用相同的随机值
- 而不是每次结果都不同
许多 Java 随机数类正是为这种可复现性而设计的。
2.4 随机性必须不可复现的情况
相反,有些随机值绝不能被复现。
典型的例子包括:
- 密码生成
- 身份验证令牌
- 会话 ID
- 一次性密钥
如果这些值是:
- 可预测的
- 可复现的
仅此就可能导致 严重的安全事件。
这就是 Java 提供
面向安全的随机生成器
与普通伪随机生成器分离的原因。
如果在不了解这种差异的情况下实现,容易出现以下情况:
- “能工作”的代码却很危险
- 代码在审查或审计时成为问题
因此请务必理解这一点。
2.5 什么是“均匀分布”?(偏差的基础)
在随机数讨论中经常出现的术语是 均匀分布。
均匀分布的含义是:
- 每个取值出现的概率相同
例如:
- 一个 1–6 的骰子,各面出现的概率相等
- 数字 0–9 出现的频率相同
这种状态即为均匀分布。
Java 的随机 API 通常假设 均匀分布 进行设计。
2.6 初学者常见的“有偏随机”示例
当你手动“调整”随机数时,可能会不小心引入偏差。
常见示例包括:
- 使用
%(取余运算符)强制范围 - 在错误的时机将
double强制转换为int - 对边界是否包含产生误解
这些错误很棘手,因为代码仍能运行,使得错误
难以察觉。
后续章节将通过具体示例说明:
- 偏差产生的原因
- 正确的写法
从而帮助你避免这些陷阱。
3. 快速入门 Math.random()
接下来,我们将查看在 Java 中生成随机数的具体方法。
第一种方法是 Math.random(),它是最简单且最常被初学者接触到的。
3.1 什么是 Math.random()?
Math.random() 是 Java 提供的一个静态方法,返回一个 大于等于 0.0 且小于 1.0 的 double 随机值。
double value = Math.random();
运行这段代码时,返回的值是:
- 大于等于 0.0
- 小于 1.0
换句话说,1.0 永远不会被包含。
3.2 为什么 Math.random() 如此易用
Math.random() 最大的优势在于
它根本不需要任何设置。
- 无需实例化类
- 无需导入语句
- 可在单行中使用
由于这些特性,它在以下场景下非常方便:
- 学习示例
- 简单演示
- 快速检查程序流程
在这些情况下使用。
3.3 使用 Math.random() 生成整数随机数
在实际程序中,你通常会想要 整数随机数
而不是 double 值。
3.3.1 生成 0 到 9 之间的随机数
int value = (int)(Math.random() * 10);
上述代码产生的值为:
- 大于等于 0
- 小于等于 9
原因如下:
Math.random()返回 0.0 到 0.999… 之间的值- 乘以 10 后得到 0.0 到 9.999…
- 强制转换为
int会截去小数部分
3.4 生成 1 到 10 时的常见陷阱
一个非常常见的初学者错误是 在何处平移起始值。
int value = (int)(Math.random() * 10) + 1;
该代码产生的随机数为:
- 大于等于 1
- 小于等于 10
如果顺序写错,写成下面这样:
// Common mistake
int value = (int)Math.random() * 10 + 1;
则代码会导致:
(int)Math.random()始终为 0- 结果始终变为 1
始终将强制转换放在括号中——这是关键点。
3.5 Math.random() 的优势与局限
优势
- 极其简洁
- 学习成本低
- 对于小型、简单的使用场景已足够
局限
- 没有可复现性(无法控制种子)
- 没有内部控制
- 不适用于安全相关的使用场景
- 在复杂场景下缺乏灵活性
尤其是当你需要:
- 在测试中得到相同的随机值
- 对行为进行细粒度控制
时,Math.random() 将不足以满足需求。
3.6 何时应使用 Math.random()
Math.random() 最适合用于:
- Java 入门阶段
- 算法解释代码
- 简单的验证示例
而在以下情况下:
- 商业应用
- 测试代码
- 与安全相关的逻辑
你应当选择 更合适的随机数生成器。
4. 理解核心类 java.util.Random
现在让我们超越 Math.random(),看看
java.util.Random 类。
Random 是 Java 中使用多年的基础类。
当你需要 “对随机数进行适当控制” 时,它就会出现。
4.1 什么是 Random 类?
Random 是用于生成伪随机数的类。
你可以通过以下方式创建实例:
import java.util.Random;
Random random = new Random();
int value = random.nextInt();
与 Math.random() 最大的区别在于
随机数生成器被视为一个对象。
这使你能够:
- 重复使用同一个生成器
- 保持行为一致
- 在设计中显式管理随机性
而这些是 Math.random() 所做不到的。
4.2 使用 Random 可以生成的随机值类型
Random 类提供了针对不同使用场景的方法。
常见示例包括:
nextInt():返回int随机值nextInt(bound):返回 0(含)到 bound(不含)之间的intnextLong():返回long随机值nextDouble():返回 0.0(含)到 1.0(不含)之间的doublenextBoolean():返回true或false
通过选择合适的方法,你可以生成 自然匹配各数据类型 的随机值。
5. 使用 Random 控制范围与可复现性
使用 java.util.Random 的最大优势之一是
对范围和可复现性的显式控制。
5.1 在特定范围内生成值
最常用的方法是 nextInt(bound)。
Random random = new Random();
int value = random.nextInt(10);
该代码产生的值为:
- 大于等于 0
- 小于 10
因此结果范围是 0 到 9。
5.2 平移范围(例如 1 到 10)
要移动范围,只需添加一个偏移量:
int value = random.nextInt(10) + 1;
这会产生以下值:
- 1(包含)
- 10(包含)
这种模式:
random.nextInt(range) + start
是 Java 中在范围内生成整数随机值的标准方式。
5.3 生成带负数范围的随机数
您也可以生成包含负值的范围。
例如,生成 -5 到 5 的值:
int value = random.nextInt(11) - 5;
说明:
nextInt(11)→ 0 到 10- 减去 5 → -5 到 5
此方法在范围符号如何时都能一致工作。
5.4 使用种子实现可复现性
Random 的另一个关键特性是 种子控制。
如果指定种子,随机序列将变得可复现:
Random random = new Random(12345);
int value1 = random.nextInt();
int value2 = random.nextInt();
只要使用相同的种子值:
- 将生成相同的随机值序列
这在以下场景中极其有用:
- 单元测试
- 仿真比较
- 调试难以定位的错误
相反,Math.random() 不允许显式的种子控制。
6. 为什么 Random 不适用于安全场景
此时,您可能会这样想:
Random 可以生成不可预测的值,那它用于安全不是很好吗?
这是一种非常常见的误解。
6.1 可预测性是核心问题
java.util.Random 使用确定性算法。
这意味着:
- 如果算法已知
- 如果观察到足够多的输出值
则未来的值可以 被预测。
对于游戏或仿真,这不是问题。
但对于安全来说,这是一大缺陷。
6.2 危险用法的具体示例
将 Random 用于以下场景是危险的:
- 密码生成
- API 密钥
- 会话标识符
- 认证令牌
即使这些值“看起来随机”,它们也不是 密码学安全 的。
6.3 “看似随机” 与 “安全” 的关键区别
关键区别在于:
- Random:快速、可复现、理论上可预测
- Secure random:不可预测、抗分析
面向安全的随机性必须在以下假设下设计:
- 攻击者知道算法
- 攻击者可以观察输出
Random 并不满足此要求。
这就是 Java 为安全使用场景提供单独类的原因。
7. 使用 SecureRandom 实现安全关键的随机性
当随机性必须 不可预测且抗攻击 时,Java 提供了 java.security.SecureRandom。
该类专为 安全敏感的使用场景 设计。
7.1 SecureRandom 与普通 Random 的区别是什么?
SecureRandom 在设计目标上与 Random 不同。
- 使用密码学强度的算法
- 从多个系统源获取熵
- 即使观察到输出,也设计为不可预测
与 Random 不同,SecureRandom 的内部状态 在实践中不可逆。
7.2 SecureRandom 的基本用法
用法与 Random 类似,但意图截然不同。
import java.security.SecureRandom;
SecureRandom secureRandom = new SecureRandom();
int value = secureRandom.nextInt(10);
这会产生以下值:
- 0(包含)
- 10(不包含)
API 故意保持相似,以便在需要时可以替换 Random。
7.3 必须使用 SecureRandom 的场景
您应在以下情况下使用 SecureRandom:
- 密码生成
- 会话 ID
- 认证令牌
- 加密密钥
在这些场景中,使用 Random 并不是“稍有风险”,而是 错误的做法。
SecureRandom 的性能开销是有意为之,并且在安全场景下是可以接受的。
8. 现代随机 API:ThreadLocalRandom 与 RandomGenerator
最近的 Java 版本提供了更高级的随机 API,以解决性能和设计方面的问题。
final.### 8.1 ThreadLocalRandom:多线程环境下的随机性
ThreadLocalRandom 针对多线程环境进行了优化。
它不再共享单个 Random 实例,而是让每个线程使用各自的生成器。
int value = java.util.concurrent.ThreadLocalRandom.current().nextInt(1, 11);
这会生成 1(含)到 11(不含)之间的值。
优势包括:
- 线程之间不存在竞争
- 并发情况下性能更佳
- 提供简洁的基于范围的 API
在并行处理时,这通常比 Random 更合适。
8.2 RandomGenerator:统一接口(Java 17+)
RandomGenerator 是为统一随机数生成而引入的接口。
它能够:
- 轻松切换算法
- 编写与实现无关的代码
- 实现更具前瞻性的设计
import java.util.random.RandomGenerator; RandomGenerator generator = RandomGenerator.getDefault(); int value = generator.nextInt(10);
这种做法推荐用于现代 Java 代码库。
9. 总结:在 Java 中选择合适的随机 API
Java 提供了多种随机数 API,因为
“随机性” 在不同场景下有不同的含义。
9.1 快速决策指南
Math.random():学习、简单演示Random:测试、仿真、可复现行为ThreadLocalRandom:多线程应用RandomGenerator:现代、灵活的设计SecureRandom:密码、令牌、安全需求
选错了 API 可能不会立刻报错,
但会在以后导致 严重问题。
9.2 需要记住的关键原则
最重要的收获是:
随机性是一种设计决策,而不仅仅是一次函数调用。
理解每个 API 背后的意图后,你就能编写出符合以下要求的 Java 代码:
- 正确
- 可维护
- 安全
并且适用于真实世界的应用场景。

