当我们抛硬币或掷骰子时,我们说我们看到了“真正”或“自然”的随机性在工作。尽管如此,还是有一些工具假装能够预测抛硬币、掷骰子或旋转轮盘的结果,特别是当满足某些上下文条件时。计算机可以使用算法通过所谓的随机数生成器来生成随机数。由于涉及算法,生成的数字被认为是伪随机的。这就是所谓的“伪”随机性。显然,伪随机数也是可预测的。为什么?
伪随机数生成器通过播种数据开始工作。这是生成器的秘密(种子),它代表用作生成伪随机数起点的数据片段。如果我们知道算法是如何工作的以及种子是什么,那么输出就是可预测的。在不知道种子的情况下,可预测性非常低。因此,为每个伪随机数生成器选择合适的种子是一个重要步骤。
在JDK 之前,Java用于生成伪随机数的API有点模糊。基本上,我们有一个包装在众所周知的java.util.Random类中的健壮API,以及Random的两个子类:SecureRandom(密码学伪随机数生成器)和ThreadLocalRandom(非线程安全伪随机数生成器)。从性能角度来看,这些伪随机数生成器之间的关系是:SecureRandom比Random慢,Random比ThreadLocalRandom慢。
除了这些类之外,我们还有SplittableRandom。这是一个非线程安全的伪生成器,能够在每次调用其split()方法时生成一个新的SplittableRandom。这样,每个线程(例如,在fork/join架构中)都可以使用自己的SplittableGenerator。
直到JDK ,伪随机数生成器的类层次结构如下图所示:
图 – JDK 之前的Java伪随机数生成器的类层次结构
如图所示,在伪随机数生成器之间切换或选择不同类型的算法确实非常繁琐。看看那个SplittableRandom——它迷失在无人区。
从JDK 开始,我们拥有了一个更灵活和强大的API来生成伪随机数。这是一个基于接口的API(随JEP 一起发布),围绕新的RandomGenerator接口展开。以下是JDK 的增强类层次结构:
图 – 从JDK 开始的Java伪随机数生成器的类层次结构
RandomGenerator接口代表了此API的顶峰。它代表了一个用于生成伪随机数的通用和统一协议。这个接口接管了Random API并添加了一些其他内容。
RandomGenerator接口通过5个子接口扩展,旨在为5种不同类型的伪随机数生成器提供特殊协议。
* StreamableGenerator 可以返回RandomGenerator对象的流
* SplitableGenerator 可以从这个生成器返回一个新的生成器(分裂自身)
* JumpableGenerator 可以跳过中等数量的抽取
* LeapableGenerator 可以跳过大量的抽取
*
ArbitrarilyJumpableGenerator 可以跳过任意数量的抽取
获取默认的RandomGenerator可以这样做(这是开始生成伪随机数的最简单方法,但你对选择什么没有控制权):
RandomGenerator defaultGenerator
= RandomGenerator.getDefault();
// 开始生成伪随机数
defaultGenerator.nextInt/Float/...();
defaultGenerator.ints/doubles/...();
除了这些接口之外,新API还附带了一个类(RandomGeneratorFactory),它是基于所选算法的伪随机数生成器的工厂。有三组新算法(很可能还会有更多),如下所示:
* LXM组
* Xoroshiro组
* Xoshiro组
突出显示的算法是默认算法(L32X64MixRandom)。根据伪随机数生成器的类型,我们可以选择所有/一些先前的算法。例如,L128X256MixRandom算法可以与SplittableGenerator一起使用,但不能与LeapableGenerator一起使用。所选算法与伪随机数生成器之间的不匹配会导致IllegalArgumentException。以下图表可以帮助您决定使用哪种算法。
图 – JDK 随机生成器的算法及其属性
此图表是通过以下代码生成的,旨在列出所有可用的算法及其属性(可流式处理、可跳跃、统计等):
Stream> all
= RandomGeneratorFactory.all();
Object[][] data = all.sorted(Comparator.comparing(
RandomGeneratorFactory::group))
.map(f -> {
Object[] obj = new Object[]{
f.name(),
f.group(),
f.isArbitrarilyJumpable(),
f.isDeprecated(),
f.isHardware(),
f.isJumpable(),
f.isLeapable(),
f.isSplittable(),
f.isStatistical(),
f.isStochastic(),
f.isStreamable()
};
return obj;
}).toArray(Object[][]::new);
可以通过名称或属性轻松选择算法。
按名称选择算法
可以通过一组静态的of()方法按名称选择算法。RandomGenerator和RandomGeneratorFactory中都有一个of()方法,可用于为特定算法创建伪随机数生成器,如下所示:
RandomGenerator generator
= RandomGenerator.of("L128X256MixRandom");
RandomGenerator generator
= RandomGeneratorFactory.of("Xoroshiro128PlusPlus")
.create();
接下来,我们可以通过调用众所周知的API(ints()、doubles()、nextInt()、nextFloat()等)来生成伪随机数。
如果我们需要特定的伪随机数生成器和算法,那么我们可以使用该生成器的of()方法,如下所示(这里,我们创建一个LeapableGenerator):
LeapableGenerator leapableGenerator
= LeapableGenerator.of("Xoshiro256PlusPlus");
LeapableGenerator leapableGenerator = RandomGeneratorFactory
.of("Xoshiro256PlusPlus").create();
在SplittableRandom的情况下,您也可以使用构造函数,但不能指定算法:
SplittableRandom splittableGenerator = new SplittableRandom();
在附带的代码中,您可以看到更多示例。
按属性选择算法
正如您在图中看到的,算法具有一组属性(是否可跳跃、是否统计等)。让我们选择一个既是统计又是可跳跃的算法:
RandomGenerator generator = RandomGeneratorFactory.all()
.filter(RandomGeneratorFactory::isLeapable)
.filter(RandomGeneratorFactory::isStatistical)
.findFirst()
.map(RandomGeneratorFactory::create)
.orElseThrow(() -> new RuntimeException(
"Cannot find this kind of generator"));
返回的算法可能是Xoshiro256PlusPlus。