随机数 验证码问题
一、前言
在日常工作当中,经常会有需要获取随机数、随机字符的需求,如:生成随机数验证码、生成随机字符串签名、生成2个数字之间的随机数等。这些场景其根本都在于随机数的生成,本文将对java当中生成随机数、随机字符等常见应用场景及获取方法进行简单小结。
二、伪随机、真随机数简介
计算机很难产生真正意义上的真随机数,通常我们所说的产生随机数,都是指伪随机数。从一定意义上来说,计算机本身几乎是不可能产生真正意义上的真随机数的,因为其一定是按照一定的运算规则来获取随机数的;当然,伪随机数的伪并不是说这个随机数就是假的,而是指,这个生成的随机数是按指定规律运算出来的相对随机的一个数。这些规律就是指各种编程语言中生成随机数的算法,java当中用的算法之一是线性同余算法。(具体介绍请参考http://t1174779123.iteye.com/blog/2037719)
三、Java本身的生成随机数相关方法简介
Java本身有2个常用的类来生成随机数,一个是直接使用java.lang.Math类中的Math.random()方法获取一个[0.0,1.0)之间的一个double类型的随机数;另一个是通过java.util.Random类,创建一个随机数发生器,然后再生成随机数。通过查看源码可以发现,Math.random()本身其实也是通过java.util.Random类来实现生成随机数的,只是说,其使用起来更加简单方便。
//--JDK1.7中,Math.random()部分源码
private static Random randomNumberGenerator;
private static synchronized Random initRNG() {
Random rnd = randomNumberGenerator;
return (rnd == null) ? (randomNumberGenerator = new Random()) : rnd;
}
public static double random() {
Random rnd = randomNumberGenerator;
if (rnd == null) rnd = initRNG();
return rnd.nextDouble();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
1、Math.random()
Java中的java.lang.Math类包含常见的一些数学公式函数,如:Math.round(n)四舍五入取整,Math.sqrt(n)计算平方根,Math.abs(n)计算绝对值等;而Math.random()函数则是获取一个[0.0,1.0)之间一个double类型的伪随机数。
通过Math.random()获取一个[0.0,1.0)之间的随机数后,我们就可以通过简单运算获取值在[m,n)之间的随机数了; 如: Math.random()*10就可以获取一个[0.0,10.0)之间的一个随机数,Math.random()*10+5就可以获取一个[5.0,15.0)之间的一个随机数,然后将获取的随机数进行数据类型转换就可以获取我们最终所需的随机数。
获取公式:(条件:0<=m<=n)
[m,n)之间:(数据类型)(m+Math.random()*(n-m));
[m,n]之间:(数据类型)(m+Math.random()*(n-m+1));
2、java.util.Random类
Java中的java.util.Random类可以创建一个随机数发生器,其构造函数有2个,分别是Random()、Random(long seed),前一个是创建不指定种子的随机数生成器,后一个是创建指定种子的随机数生成器,然后通过生成器生成随机数。
种子,指生成随机数算法的起始数字,和生成的随机数的区间没有任何关系。Random()构造函数其实默认会指定种子,老版本的JDK用的是System.currentTimeMillis()方法获取当前计算机时间作为种子,而新版本的JDK用的是System.nanoTime()方法获取当前cpu核心纳秒级时间作为种子。(两者区别请参考http://blog.csdn.net/dliyuedong/article/details/8806868)
//笔者jdk1.7.0.79版本的Random()构造函数源码
public Random() {
this(seedUniquifier() ^ System.nanoTime());
}
- 1
- 2
- 3
- 4
指定种子的话,如果种子值相同,无论执行多少次,其将生成同一随机数列。
例1:
public class TestRandom {
public static void main(String[] args) {
Random random1 = new Random(10);
Random random2 = new Random(10);
int random1_1 = random1.nextInt(100);
int random1_2 = random1.nextInt(100);
int random2_1 = random2.nextInt(100);
int random2_2 = random2.nextInt(100);
System.out.println("random1_1->" + random1_1);
System.out.println("random1_2->" + random1_2);
System.out.println("random2_1->" + random2_1);
System.out.println("random2_2->" + random2_2);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
无论执行多少次,其输出结果均为:
random1_1->13
random1_2->80
random2_1->13
random2_2->80
- 1
- 2
- 3
- 4
Random类中有许多生成随机数的方法,如Math.random()其实就是调用Random类中的nextDouble()方法来获取随机数。这里对其中常用的方法简单说明一下:
//返回下一个伪随机数,它是此随机数生成器的序列中均匀分布的 int 值
public int nextInt();
//返回一个伪随机数,它是取自此随机数生成器序列的、在(包括和指定值(不包括)之间均匀分布的int值
public int nextInt(int n);
// 返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的 long 值
public long nextLong();
// 返回下一个伪随机数,它是取自此随机数生成器序列的、在0.0和1.0之间均匀分布float值
public float nextFloat();
// 返回下一个伪随机数,它是取自此随机数生成器序列的、在0.0和1.0之间均匀分布的
public double nextDouble();
// 返回下一个伪随机数,它是取自此随机数生成器序列的均匀分布的boolean值。
public boolean nextBoolean();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
基本上有以上常用方法,就可以通过简单运算获取我们最终所需的随机数了,运算方法可以参考第1点说明;当然,Random类还有其它获取随机数的方法,感兴趣的朋友可以去了解一下。
四、常见的随机数使用场景及生成方法
(注意值的闭包区间)
1、获取指定数值内的随机数
例2:
//获取[0,n)之间的一个随机整数
public static int getRandom(int n) {
return (int) (Math.random() * n);
}
- 1
- 2
- 3
- 4
2、获取2个数字区间内的随机数
例3:
//获取[m,n]之间的随机数(0<=m<=n)
public static int getRandomBetweenNumbers(int m,int n){
return (int)(m + Math.random() * (n - m + 1));
}
- 1
- 2
- 3
- 4
此方法可用于生成随机验证码。
3、获取指定长度的随机字符串
Java本身并没有生成随机字符串的方法,但我们可以通过java自带的随机数方法运算获取所需的随机字符串。
例4:
//获取指定位数的随机字符串(包含小写字母、大写字母、数字,0<length)
public static String getRandomString(int length) {
//随机字符串的随机字符库
String KeyString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
StringBuffer sb = new StringBuffer();
int len = KeyString.length();
for (int i = 0; i < length; i++) {
sb.append(KeyString.charAt((int) Math.round(Math.random() * (len - 1))));
}
return sb.toString();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
在这个例子中,我们先自定义一个字符串库KeyString ,然后通过Math.random()方法获取KeyString长度内的一个随机数,接着再获取该随机数对应KeyString中相应位置的一个字符,最后将随机获取并组装好的字符串返回。
大家可以发现,这个例子获取的随机字符串可能包含小写字母、大写字母、数字;如果说还需要包含其它字符的话,如%、#、/、* 等特殊字符,只需将相应字符添加到字符串库KeyString 中去即可;同理,若想生成只包含小写字母或者只包含数字的字符串,也只需修改字符串库KeyString 即可。可以说,这几乎是获取随机字符串的一个“万金油”方法。
4、随机生成指定概率的数字
获取指定值内的随机数,从理论上来说,每个数字出现概率都是一样的,但是,我们可以通过一定的运算实现按一定的概率获取数字。
如:我们想随机生成0、1这两个数字,但是呢,我们希望0出现的概率为70%,1出现的概率为30%;这个时候,我们可以通过用一定的随机数区间值来分别表示0、1,从而实现按概率获取随机数。
例5:
//输出0或者1;0出现的概率为70%,1出现的概率为30%
public class TestRandom {
public static void main(String[] args){
Random random = new Random();
int n = random.nextInt(100);
if(n < 70){
System.out.println("0");
}else{
System.out.println("1");
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
由以上示例可知,通过一定的运算,我们就可以按一定概率获取数字,非常简单的抽奖小游戏就可以使用这种方式来设置中奖概率。
例6:
//简单的按概率获取数字,注意定义数组时的概率总和需为100%
public class TestRandom {
public static void main(String[] args) {
//各数字出现的概率分别是:1(10%)、2(20%)、3(30%)、4(40%)
float[][] array = new float[][]{{1,10},{2,20},{3,30},{4,40}};
int n = luckDraw(array,new Random());
if(1 == n){
System.out.println("一等奖");
}else if(2 == n){
System.out.println("二等奖");
}else if(3 == n){
System.out.println("三等奖");
}else if(4 == n){
System.out.println("谢谢参与");
}
}
//简单幸运抽奖
public static int luckDraw(float[][] array,Random random){
int n = 10000; //总值
int length = array.length;
int random_num = random.nextInt(n); //随机数
for(int i=0;i<length;i++){
float before_chance = 0; //当前概率值之前的总概率
for(int j=0;j<i;j++){
before_chance = before_chance + array[j][1];
}
int value = (int) (array[i][1] / 100 * n); //区间
int up = (int) (before_chance / 100 * n); //大于等于该值
int under = up + value; //小于该值
if((up <= random_num) && (random_num < under)){
return (int) array[i][0];
}
}
return 0;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
需要指出的是,这只是一个非常简单的小示例,只适用于要求较低的场景。真正严格意义上的抽奖有许多要求,如总出奖数量、每个奖的概率随时间的推移而变化等,有专门的一些算法去实现抽奖中的各种概率问题,感兴趣的朋友可以去了解一下。
五、总结
1、Math.random()方法和Random类获取随机数的实现方法相同,只是相对而言,Math.random()的使用更简洁便利,而Random类的方法更丰富、使用更加灵活。
2、高并发系统中,即使种子为毫秒级,java.util.Random类获取的随机数,还是可能会相同,从而给系统带来潜在风险。
3、随机数是相对随机的,伪随机数生成效率高,而真随机数可能需要一定的硬件支持且生成效率低;并没有说哪个就一定好,凡事都有相对性,我们可以根据自己业务场景所需选择。