本章介绍Java编程中一些常用的基础类,探讨它们的用法、应用和实现原理,这些类有:
- 各种包装类
- 文本处理的类String和StringBuilder
- 数组操作的类Arrays
- 日期和时间处理
- 随机
7.1 包装类
7.2 剖析String
7.3 剖析SringBuilder
7.4 剖析Arrays
7.4.3 实现原理
下面介绍Arrays的方法的实现原理。hashCode()的实现我们已经介绍了;fill和equals等的实现都很简单,循环操作即可,不再赘述;下面主要介绍二分查找和排序的实现代码。
-
二分查找
-
排序
7.4.4 小结
其实,Arrays中包含的数组方法是比较少的,很多常用的操作没有,比如,Arrays的binarySearch只能针对已排序数组进行查找,那没有排序的数组怎么方便查找呢?
Apache 有一个开源包 ( commons.apache.org/proper/comm… ), 里面有个类ArrayUtils(位于包org.apache.commongs.lang3),包含了更多的常用数组操作,这里就不列举了。
数组是计算机程序中的基本数据结构,Arrays类以及ArrayUtils类封装了关于数组的常见操作,使用这些方法,避免“重复发明轮子”吧。
7.5 剖析日期和时间
7.6 随机
本节,我们来讨论随机,随机是计算机程序中一个非常常见的需求,比如:
- 各种游戏中有大量的随机,比如扑克游戏中的洗牌。
- 微信抢红包,抢的红包金额是随机的。
- 北京购车摇号,谁能摇到是随机的。
- 给用户生成随机密码。
我们首先来介绍Java对随机的支持,同时介绍其实现原理,然后针对一些实际场景,包括洗牌、抢红包、摇号、随机高强度密码、带权重的随机选择等,讨论如何应用随机。先来看如何使用最基本的随机。
7.6.1 Math.random
Java中,对随机最基本的支持是Math类中的静态方法random(), 它生成一个0 ~ 1的随机数,类型为double, 包括0但不包括1。比如,随机生成并输出3个数:
for(int i=0;i<3;i++){
System.out.println(Math.random());
}
笔者的计算机中的一次运行,输出为:
0.47849323243242434
0.54352432352355242424
0.723424323405887823432
每次运行,输出都不一样。Math.random()是如何实现的呢? 我们来看相关代码(Java 7):
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();
}
内部它使用了一个Random类型的静态变量randomNumberGenerator,调用random()就是调用该变量的nextDouble()方法,这个Random变量只有在第一次使用的时候才创建。
下面我们来看这个Random类,它位于java.util下。
7.6.2 Random
Random类提供了更为丰富的随机方法,它的方法不是静态方法,使用Random, 先要创建一个Random实例,看个例子:
Random rnd = new Random();
System.out.println(rnd.nextInt());
System.out.println(rnd.nextInt(100));
笔者计算机中的一次运行,输出为:
-1516612608
23
nextInt()产生一个随机的int, 可能是正数,也可能是负数,nextInt(100)产生一个随机int,范围是0 ~ 100, 包括0不包括100。除了nextInt,还有一些别的方法:
public long nextLong() //随机生成一个long
public boolean nextBoolean() //随机生成一个boolean
public void nextBytes(byte[] bytes) //产生随机字节,字节个数就是bytes的长度
public float nextFloat() //随机浮点数,从0到1,包括0不包括1
public double nextDouble() //随机浮点数,从0到1,包括0不包括1
除了默认构造方法,Random类还有一个构造方法,可以接受一个long类型的种子参数:
public Random(long seed)
种子决定了随机产生的序列,种子相同,产生的随机数字序列就是相同的。 看个例子:
Random rnd = new Random(20160824);
for(int i=0;i<5;i++){
System.out.print(rnd.nextInt(100) + " ");
}
种子为20160824,产生5个0~100的随机数,输出为:
69 13 13 94 50
这个程序无论执行多少遍,在哪执行,输出结果都是相同的。
除了在构造方法中指定种子,Random类还有一个setter实例方法:
synchronized public void setSeed(long seed);
其效果与在构造方法中指定种子是一样的。
为什么要指定种子呢?指定种子还是真正的随机吗?指定种子是为了实现可重复的随机。 比如用于模拟测试程序中,模拟要求随机,但测试要求可重复。在北京购车摇号程序中,种子也是指定的,后面我们还会介绍。种子到底扮演了什么角色呢?随机到底是如何产生的呢?让我们看下随机的基本原理。
7.6.3 随机的基本原理
Random产生的随机数不是真正的随机数,相反,它产生的随机数一般称为伪随机数。真正的随机数比较难以产生,计算机程序中的随机数一般都是伪随机数。
伪随机数都是基于一个种子数的,然后没需要一个随机数,都是对当前种子进行一些数学运算,得到一个数,基于这个数得到需要的随机数和新的种子。
数学运算是固定的,所以种子确定后,产生的随机数序列就是确定的,确定的数字序列当然不是真正的随机数,但种子不同,序列就不同,每个序列中的数字的分部也都是比较随机和均匀的,锁称之为伪随机数。
Random的默认构造方法中没有传递种子,它会自动生成一个种子,这个种子数是一个真正的随机数,如下所示(Java 7):
private static final AtomicLong seedUniquifier = new AtomicLong(868222807148012L);
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;
}
}
种子是seedUniquifier()与System.nanoTime()按位异或的结果,System.nanoTime()返回一个更高精度(纳秒)的当前时间,seedUniquifer()里面的代码设计一些多线程相关的知识,我们后续章节再介绍,简单地说,就是返回当前seedUniquifier(current变量)与一个常数181783497276652981L相乘的结果(next变量),然后,设置seedUniquifier的值为next,使用循环和compareAndSet都是为了确保在多线程的环境下不会有两次调用返回相同的值,保证随机性。
有了种子数之后,其他数是怎么生成的呢?我们来看一些代码:
public int nextInt(){
return next(32);
}
public long nextLong(){
return ((long)(next(32)) << 32) + next(32);
}
public float nextFloat(){
return next(24) / ((float)(1 << 24));
}
public boolean nextBoolean(){
return next(1) != 0;
}
它们都调用了next(int bits),生成指定位数的随机数,我们来看下它的代码:
private static final long multiplier = 0x5DEECE66DL;
private static final long addend = 0xBL;
private static final long mask = (1L << 48) - 1;
protected int next(int bits){
long oldseed, nextseed;
AtomicLong seed = this.seed;
do{
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
}while(!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}
简单地说,就是使用了如下公式:
nextseed = (oldseed * multiplier + addend) & mask;
旧的种子(oldseed)乘以一个数(multiplier),加上一个数addend, 然后取低48位作为结果(mask相与)。
为什么采用这个方法? 这个方法为什么可以产生随机数?这个方法的名称叫线性同余随机数生成器(linear congruential pseudorandom number generator), 描述在《计算机程序设计艺术》一书中,随机的理论是一个比较复杂的话题,超出了本书的范畴,我们就不讨论了。
我们需要知道的基本原理是:随机数基于一个种子,种子固定,随机数序列就固定,默认构造方法中,种子是一个真正的随机数。
理解了随机的基本概念和原理,我们来看一些应用场景,包括随机密码、洗牌、带权重的随机选择、微信抢红包算法,以及北京购车摇号算法。