在很多程序当中都要用到随机数。例如某个软件在登录时需要填写一个四位数的验证码,这个验证码就是一个典型的随机数。位于java.util包下的Random类是一个专门用于生成随机数的类,程序员使用这个类可以生成类型各异的随机数。
11.4.1随机数生成方法
Random类中用于生成随机数的方法如表11-6所示。
表11-6 Random类生成随机数的方法
方法 | 作用 |
float nextFloat()/double nextDouble() | 产生[0,1)区间内float/double型均匀分布的随机数 |
float nextFloat(a)/double nextDouble(a) | 产生[0,a)区间内float/double型均匀分布的随机数 |
float nextFloat(a,b)/double nextDouble(a,b) | 产生[a,b)区间内float/double型均匀分布的随机数 |
double nextGaussian() | 产生正态分布双精度浮点型随机数 |
boolean nextBoolean() | 产生boolean型随机数 |
int nextInt()/long nextLong() | 产生int/long型均匀分布的随机数 |
int nextInt(a)/long nextLong(a) | 产生[0,a)区间内int/long型均匀分布的随机数 |
int nextInt(a,b)/long nextLong(a,b) | 产生产生[a,b)区间内int/long型均匀分布的随机数 |
void nextBytes(byte[] b) | 一次性产生一组byte型的随机数 |
IntStream ints() | 一次性产生无限个int型的随机数 |
IntStream ints(n) | 一次性产生n个int型的随机数 |
IntStream ints(a,b) | 一次性产生无限个在[a,b)区间内的int型的随机数 |
IntStream ints(n,a,b) | 一次性产生n个在[a,b)区间内的int型的随机数 |
LongStream longs() | 一次性产生无限个long型的随机数 |
LongStream longs(n) | 一次性产生n个long型的随机数 |
LongStream longs(a,b) | 一次性产生无限个在[a,b)区间内的long型的随机数 |
LongStream longs(n,a,b) | 一次性产生n个在[a,b)区间内的long型的随机数 |
DoubleStream doubles() | 一次性产生无限个在[0,1)区间内的double型的随机数 |
DoubleStream doubles(n) | 一次性产生n个在[0,1)区间内的double型的随机数 |
DoubleStream doubles(a,b) | 一次性产生无限个在[a,b)区间内的double型的随机数 |
DoubleStream doubles(n,a,b) | 一次性产生n个在[a,b)区间内的double型的随机数 |
从表11-6可以看到: nextBytes()方法能够一次性产生一组byte型的随机数。很多读者都会认为:既然nextBytes()方法能够一次性产生一组随机数,那么这个方法的返回值应该是byte型数组。但事实上,nextBytes()方法的返回值是void类型的,也就是说这个方法没有返回值。没有返回值的情况下,方法运行产生的随机数被放在了哪里呢?nextBytes()方法的参数是byte型数组,方法运行的结果就存放在这个数组中。这种设计方式可以理解为:给方法传递的byte型数组是一个空的篮子,当方法运行结束之后篮子里就装满了随机数。方法运行所产生随机数的多少,完全取决于篮子的大小,程序员要做的只是把一个空篮子,也就是一个byte型数组放到方法当中就可以了。
早期的Random类只能产生一组byte型的随机数,这导致随机数的类型有很大局限性。从JDK1.8开始, Random类又增加了几个一次性产生多个随机数的方法。这些方法分别是ints()、longs()以及doubles(),它们分别可以一次性产生一组int、long以及double型的随机数。这些方法的返回值都是流(Stream),目前为止本书还没有讲解关于流的知识,读者只需要把流简单的理解为盛放数据的容器即可。通过toArray()方法可以把流转换成一个数组。转换之后,数组中保存的还是流当中所保存的那些元素,这样的话,通过数组就能很容易的访问方法所产生的随机数了,例如:
IntStream is = rd.ints(5,10,20);//产生一组随机数并保存到流中
int[] array = is.toArray();//把流转换为数组
for(int i:array){//使用增强型for循环变量数组元素
System.out.println(i);
}
下面的【例11_16】展示了使用Random类产生double型随机数的具体过程。
【例11_16 产生double型随机数】
Exam11_16.java
import java.util.Random;
import java.util.stream.DoubleStream;
public class Exam11_16 {
public static void main(String[] args) {
Random rd = new Random();
System.out.println("使用nextDouble()方法产生一组在区间[0,1)内的double型随机数");
for (int i=0;i<5;i++){
double d = rd.nextDouble();
System.out.println(d);
}
System.out.println("使用doubles()方法产生一组在区间[2,5)内的double型随机数");
DoubleStream ds = rd.doubles(5,2,5);
double[] array = ds.toArray();
for(double d :array){
System.out.println(d);
}
}
}
由于【例11_16】的运行结果是随机的,所以此处没有给出运行结果截图,读者可以自行运行【例11_16】以体验随机数的产生效果,并且可以尝试使用相应的方法产生其他类型的随机数。
11.4.2随机数的生成原理
【例11_16】的代码在创建Random类对象时使用的是无参数的构造方法,实际上Random类还有一个构造方法,它的形式为:
public Random(long seed) |
可以看到,构造方法有一个long型的参数seed,这个参数被称为“随机数的种子”。如果在调用构造方法创建Random类对象时设置了随机数的种子,那么Random类对象的各种方法将不会产生随机数,请看下面的【例11_17】
【例11_17 设置随机数的种子】
Exam11_17.java
import java.util.Random;
public class Exam11_17 {
public static void main(String[] args) {
Random rd1 = new Random(100);
Random rd2 = new Random(100);
for(int i=0;i<5;i++){
System.out.println("A:" + rd1.nextInt() + " B:" + rd2.nextInt());
}
}
}
在【例11_17】中,在创建rd1和rd2时所设置的随机数种子都是100。程序中用for循环的形式使rd1和rd2连续的产生了5个int型随机数,并且用输出语句把所产生的随机数打印到控制台上。为了区分随机数是由哪一个Random类对象产生的,输出语句特意在rd1所产生的随机数前面加上了“A:”而在rd2所产生的随机数前面加上了“B:”。【例11_17】的运行结果如图11-15所示。
图11-15【例11_17】运行结果
从图11-15可以很明显的看出:rd1和rd2每次所产生的int型随机数都是完全相同的。实际上,如果多次运行程序,每次的运行结果也都与图11-15完全相同。这说明在创建Random类对象时如果设置了随机数种子,那么Random类对象所产生的不再是随机数,而是固定的数。如果为几个Random类对象设置了相同的随机数种子,那么这些对象所产生的固定的数也是完全相同的,即使多次运行程序也不会发生改变。
为什么Random类对象在被设置了种子之后就不再产生随机数呢?这是因为Random类本质上只是一个能够产生一系列数字的类,它所产生的这一系列数字是根据以下公式计算出的:
an+1=(b*an+c) mod m(n>=0,a0=d,d<=m) |
公式中,b=0x5DEECE66DL,c=0xBL,m=0xFFFFFFFFFFFFL。从公式中可以看出:an+1的产生依赖于an,而在计算an+1的时候,用到了参数b、c、m,这些参数的值都已经被确定,因此只要确定了a0的值就能根据公式计算出a1的值,有了a1的值就能计算出a2的值,以此类推,就能够一直计算出an以及an+1的值。现在的关键的问题是:a0的值是多少?从公式中可以看到:a0的值是d,而d的值就是创建Random类对象时所设定的种子。这说明:当种子一旦被确定,a0的值就被确定下来,而a0的值被确定了,a1的值也随之确定,之后每一个数字都是确定的,根本不是随机的。因此在【例11_17】中,创建rd1和rd2时设定了相同的种子,会导致它们连续调用nextInt()方法时所产生的数字也是完全相同的。此外还需要说明:以上用于计算数列的公式只是最原始的计算公式,Random类对象在产生各种类型随机数的过程中还要在原始计算公式上进行一定程度的加工,最终才能产生符合需求的数字序列。
创建Random对象时如果调用的是无参数的构造方法,实际上就是没有人为指定随机数种子,在这种情况下,虚拟机会先获取系统时间,然后把系统时间按照特定的算法公式转换成一个数字,最终把这个数字作为随机数的种子。由系统时间转换出来的数字重复概率极低,这就直接导致随机数种子重复概率也极低,所以最终产生数字序列重复的概率也同样非常低,给人们带来的感觉就是:由Random类对象产生的数字序列没有任何规律,是随机产生的。其实这些数字序列从严格意义上来讲只能被称为“伪随机数”。虽然是伪随机数,但是经过人们大量的实践证明:由Ramdom类产生的伪随机数已经非常接近真实随机数,所有读者完全可以把这些伪随机数当作真实随机数来使用。
本文字版教程还配有更详细的视频讲解,小伙伴们可以点击这里观看。