一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情。
本文将介绍Java中的随机方法Random(),为什么它们是伪随机的,以及如何优化使它更加“随机”
什么是伪随机?
伪随机数是看似随机但本质上是固定的周期性序列,即规则随机数
只要这个随机数是确定性算法产生的,就是伪随机数,只能通过不断的算法优化让随机数更接近随机
Math.random()
简介
Java中我们可以通过Math.random()中获得随机数,它返回0~1之间的一个double值
public static void main(String[] args) {
double random = Math.random();
System.out.println("random = " + random);
}
复制代码
输出:random = 0.10842545820862226
如果你想得到一个int类型的整数,你只需要将上面的结果转换为int类型。 例如,得到0~100之间的int整数。方法如下。
double d = Math.random();
int i = (int) (d*100);
复制代码
随机类
Random 类提供了两个构造函数:
public Random() {
}
public Random(long seed) {
}
复制代码
一个是默认构造函数,一个能够传入随机seed
然后从 Random 对象中获取随机数:
int r = random.nextInt(100);
复制代码
常用API
boolean nextBoolean()
void nextBytes(byte[] buf)
double nextDouble()
float nextFloat()
int nextInt()
int nextInt(int n)
long nextLong()
synchronized double nextGaussian() // 返回一个正态分布的 double 值,其平均值为0.0标准差为1.0。
synchronized void setSeed(long seed) // 设置一个Long类型的seed
复制代码
样例
private static void testRandom(Random random) {
boolean b = random.nextBoolean();
System.out.println("boolean = " + b);
byte[] buf = new byte[5];
random.nextBytes(buf);
System.out.println("byte = " + Arrays.toString(buf));
double d = random.nextDouble();
System.out.println("double = " + d);
float f = random.nextFloat();
System.out.println("float = " + f);
int i0 = random.nextInt();
System.out.println("没有seed = " + i0);
int i1 = random.nextInt(100);
System.out.println("seed = " + i1);
double gaussian = random.nextGaussian();
System.out.println("gaussian = " + gaussian);
long l = random.nextLong();
System.out.println("long = " + l);
}
public static void main(String[] args) {
testRandom(new Random());
System.out.println();
testRandom(new Random(10));
System.out.println();
testRandom(new Random(10));
}
复制代码
执行输出
boolean = true
byte = [116, -117, 27, -44, -71]
double = 0.32145597995841546
float = 0.130032
没有seed = -1309582825
seed = 32
gaussian = -0.5114081918886987
long = 1980360936763293403
boolean = true
byte = [-8, 22, 21, 114, 58]
double = 0.4129126974821382
float = 0.67215943
没有seed = 1048475594
seed = 88
gaussian = 1.1329921492850181
long = -2651111998922877327
boolean = true
byte = [-8, 22, 21, 114, 58]
double = 0.4129126974821382
float = 0.67215943
没有seed = 1048475594
seed = 88
gaussian = 1.1329921492850181
long = -2651111998922877327
复制代码
可以看到,在运行期间,如果seed相同,则随机值相同。
所以同一个seed,产生N个随机数。当你设置seed时,确定了N个随机数。产生相同次数的随机数是相同的。而且如果使用相同的seed创建了两个 Random 实例,则对每个实例应用相同的方法调用序列,它们将生成并返回相同的序列。
原理
我们从 Random 类的构造函数和属性开始:
private final AtomicLong seed;
private static final long multiplier = 0x5DEECE66DL;
private static final long addend = 0xBL;
private static final long mask = (1L << 48) - 1;
private static final double DOUBLE_UNIT = 0x1.0p-53; // 1.0 / (1L << 53)
private static final AtomicLong seedUniquifier
= new AtomicLong(8682522807148012L);
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
private static long seedUniquifier() {
for (;;) {
long current = seedUniquifier.get();
long next = current * 181783497276652981L;
if (seedUniquifier.compareAndSet(current, next))
return next;
}
}
public Random(long seed) {
if (getClass() == Random.class)
this.seed = new AtomicLong(initialScramble(seed));
else {
this.seed = new AtomicLong();
setSeed(seed);
}
}
synchronized public void setSeed(long seed) {
this.seed.set(initialScramble(seed));
haveNextNextGaussian = false;
}
复制代码
有两种构造函数,一种是无参数的,一种是可传递给seed的
seed是第一个用于生成随机数的值。其机制是通过一个函数将seed的值转化为随机数空间中的一个点,得到的随机数均匀地分散在空间中,后续的随机数与第一个随机数相关
seed由seedUniquifier()^System.nanoTime()生成,使用CAS自旋锁实现。使用System.nanoTime()方法,得到一个纳秒时间量,然后连续乘以1817834972766981L直到一次乘法前后结果相同,这是为了增加随机性,这里nanotime可以认为是真正的随机数
但有必要提一下,nanoTime 与我们常用的currenttime方法不同,它返回的是一个随机数,而不是1970-1-1至今的时间
所以不要随意设置随机seed。如果你运行的次数多,你可能会得到相同的随机数。Random生成的seed已经可以满足你平时的需要
Java自带的Random()也容易被破解,攻击者可以通过得到一个一定长度的随机数序列来推断你的seed,然后就可以预测下一个随机数
如何优化随机
主要考虑的是生成的随机数不能重复,如果重复就会重新生成一个。可以使用数组或者Set存储判断是否包含重复的随机数,递归地重新生成一个新的随机数。