Java 随机数详解:Math.random()、Random、SecureRandom 与范围模式

.## 1. 本文你将学到的内容

当你在 Java 中尝试使用“随机数”时,你会很快遇到多种选项,例如 Math.random()RandomSecureRandom
很多人最终会想:“我该用哪一个?”

在本节中,我们将直接给出结论,并阐明阅读本文至结束后你能够做到的事情。先把大局弄清楚,再深入细节和代码,后面的章节就会容易得多。

目次

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(不含)之间的 int
  • nextLong():返回 long 随机值
  • nextDouble():返回 0.0(含)到 1.0(不含)之间的 double
  • nextBoolean():返回 truefalse

通过选择合适的方法,你可以生成 自然匹配各数据类型 的随机值。

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:ThreadLocalRandomRandomGenerator

最近的 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 代码:

  • 正确
  • 可维护
  • 安全

并且适用于真实世界的应用场景。