Math 类、Random 类、BigDecimal 类

Math 类

Java 提供了 Math 工具类来完成复杂的运算,Math 类是一个工具类,它的构造器被定义成 private 的,因此无法创建 Math 类的对象:Math 类中的所有方法都是类方法,可以直接通过类名来调用它们。Math 类除提供了大量静态方法之外,还提供了两个类变量:PI 和 E,正如它们名字所暗示的,它们的值分别等于 π 和 e。

Math 类的所有方法名都明确标识了该方法的作用,读者可自行查阅 API 来了解 Math 类各方法的说明。下面程序示范了 Math 类的用法。

public class MathTest {
    public static void main(String[] args) {
        /*---------下面是三角运算---------*/
        // 将弧度转换角度
        System.out.println("Math.toDegrees(1.57):" + Math.toDegrees(1.57));
        // 将角度转换为弧度
        System.out.println("Math.toRadians(90):" + Math.toRadians(90));
        // 计算反余弦,返回的角度范围在 0.0 到 pi 之间。
        System.out.println("Math.acos(1.2):" + Math.acos(1.2));
        // 计算反正弦;返回的角度范围在 -pi/2 到 pi/2 之间。
        System.out.println("Math.asin(0.8):" + Math.asin(0.8));
        // 计算反正切;返回的角度范围在 -pi/2 到 pi/2 之间。
        System.out.println("Math.atan(2.3):" + Math.atan(2.3));
        // 计算三角余弦。
        System.out.println("Math.cos(1.57):" + Math.cos(1.57));
        // 计算值的双曲余弦。
        System.out.println("Math.cosh(1.2 ):" + Math.cosh(1.2));
        // 计算正弦
        System.out.println("Math.sin(1.57 ):" + Math.sin(1.57));
        // 计算双曲正弦
        System.out.println("Math.sinh(1.2 ):" + Math.sinh(1.2));
        // 计算三角正切
        System.out.println("Math.tan(0.8 ):" + Math.tan(0.8));
        // 计算双曲正切
        System.out.println("Math.tanh(2.1 ):" + Math.tanh(2.1));
        // 将矩形坐标 (x, y) 转换成极坐标 (r, thet));
        System.out.println("Math.atan2(0.1, 0.2):" + Math.atan2(0.1, 0.2));
        /*---------下面是取整运算---------*/
        // 取整,返回小于目标数的最大整数。
        System.out.println("Math.floor(-1.2 ):" + Math.floor(-1.2));
        // 取整,返回大于目标数的最小整数。
        System.out.println("Math.ceil(1.2):" + Math.ceil(1.2));
        // 四舍五入取整
        System.out.println("Math.round(2.3 ):" + Math.round(2.3));
        /*---------下面是乘方、开方、指数运算---------*/
        // 计算平方根。
        System.out.println("Math.sqrt(2.3 ):" + Math.sqrt(2.3));
        // 计算立方根。
        System.out.println("Math.cbrt(9):" + Math.cbrt(9));
        // 返回欧拉数 e 的n次幂。
        System.out.println("Math.exp(2):" + Math.exp(2));
        // 返回 sqrt(x2 +y2)
        System.out.println("Math.hypot(4 , 4):" + Math.hypot(4, 4));
        // 按照 IEEE 754 标准的规定,对两个参数进行余数运算。
        System.out.println("Math.IEEEremainder(5 , 2):" + Math.IEEEremainder(5, 2));
        // 计算乘方
        System.out.println("Math.pow(3, 2):" + Math.pow(3, 2));
        // 计算自然对数
        System.out.println("Math.log(12):" + Math.log(12));
        // 计算底数为 10 的对数。
        System.out.println("Math.log10(9):" + Math.log10(9));
        // 返回参数与 1 之和的自然对数。
        System.out.println("Math.log1p(9):" + Math.log1p(9));
        /*---------下面是符号相关的运算---------*/
        // 计算绝对值。
        System.out.println("Math.abs(-4.5):" + Math.abs(-4.5));
        // 符号赋值,返回带有第二个浮点数符号的第一个浮点参数。
        System.out.println("Math.copySign(1.2, -1.0):" + Math.copySign(1.2, -1.0));
        // 符号函数;如果参数为 0,则返回 0;如果参数大于 0,
        // 则返回 1.0;如果参数小于 0,则返回 -1.0。
        System.out.println("Math.signum(2.3):" + Math.signum(2.3));
        /*---------下面是大小相关的运算---------*/
        // 找出最大值
        System.out.println("Math.max(2.3 , 4.5):" + Math.max(2.3, 4.5));
        // 计算最小值
        System.out.println("Math.min(1.2 , 3.4):" + Math.min(1.2, 3.4));
        // 返回第一个参数和第二个参数之间与第一个参数相邻的浮点数。
        System.out.println("Math.nextAfter(1.2, 1.0):" + Math.nextAfter(1.2, 1.0));
        // 返回比目标数略大的浮点数
        System.out.println("Math.nextUp(1.2 ):" + Math.nextUp(1.2));
        // 返回一个伪随机数,该值大于等于 0.0 且小于 1.0。
        System.out.println("Math.random():" + Math.random());
    }
}

上面程序中关于 Math 类的用法几乎覆盖了 Math 类的所有数学计算功能,读者可参考上面程序来学习 Math 类的用法。

Java 7 的 ThreadLocalRandom 与 Random

Random 类专门用于生成一个伪随机数,它有两个构造器:一个构造器使用默认的种子(以当前时间作为种子),另一个构造器需要程序员显式传入一个 long 型整数的种子。

ThreadLocalRandom 类是 Java 7 新增的一个类,它是 Random 的增强版。在并发访问的环境下,使用 ThreadLocalRandom 来代替 Random 可以减少多线程资源竞争,最终保证系统具有更好的线程安全性。

ThreadLocalRandom 类的用法与 Random 类的用法基本相似,它提供了一个静态的 current() 方法来获取 ThreadLocalRandom 对象,获取该对象之后即可调用各种 nextXxx() 方法来获取伪随机数了。

ThreadLocalRandom 与 Random 都比 Math 的 random() 方法提供了更多的方式来生成各种伪随机数,可以生成浮点类型的伪随机数,也可以生成整数类型的伪随机数,还可以指定生成随机数的范围。关于 Random 类的用法如下程序所示。

public class RandomTest {
    public static void main(String[] args) {
        Random rand = new Random();
        System.out.println("rand.nextBoolean():" + rand.nextBoolean());
        byte[] buffer = new byte[16];
        rand.nextBytes(buffer);
        System.out.println(Arrays.toString(buffer));
        // 生成0.0~1.0之间的伪随机double数
        System.out.println("rand.nextDouble():" + rand.nextDouble());
        // 生成0.0~1.0之间的伪随机float数
        System.out.println("rand.nextFloat():" + rand.nextFloat());
        // 生成平均值是 0.0,标准差是 1.0的伪高斯数
        System.out.println("rand.nextGaussian():" + rand.nextGaussian());
        // 生成一个处于int整数取值范围的伪随机整数
        System.out.println("rand.nextInt():" + rand.nextInt());
        // 生成0~26之间的伪随机整数
        System.out.println("rand.nextInt(26):" + rand.nextInt(26));
        // 生成一个处于long整数取值范围的伪随机整数
        System.out.println("rand.nextLong():" + rand.nextLong());
    }
}

从上面程序中可以看出,Random 可以提供很多选项来生成伪随机数。

Random 使用一个 48 位的种子,如果这个类的两个实例是用同一个种子创建的,对它们以同样的顺序调用方法,则它们会产生相同的数字序列。

下面就对上面的介绍做一个实验,可以看到当两个 Random 对象种子相同时,它们会产生相同的数字序列。值得指出的,当使用默认的种子构造 Random 对象时,它们属于同一个种子。

public class SeedTest {
    public static void main(String[] args) {
        Random r1 = new Random(50);
        System.out.println("第一个种子为50的Random对象");
        System.out.println("r1.nextBoolean():\t" + r1.nextBoolean());
        System.out.println("r1.nextInt():\t\t" + r1.nextInt());
        System.out.println("r1.nextDouble():\t" + r1.nextDouble());
        System.out.println("r1.nextGaussian():\t" + r1.nextGaussian());
        System.out.println("---------------------------");
        Random r2 = new Random(50);
        System.out.println("第二个种子为50的Random对象");
        System.out.println("r2.nextBoolean():\t" + r2.nextBoolean());
        System.out.println("r2.nextInt():\t\t" + r2.nextInt());
        System.out.println("r2.nextDouble():\t" + r2.nextDouble());
        System.out.println("r2.nextGaussian():\t" + r2.nextGaussian());
        System.out.println("---------------------------");
        Random r3 = new Random(100);
        System.out.println("种子为100的Random对象");
        System.out.println("r3.nextBoolean():\t" + r3.nextBoolean());
        System.out.println("r3.nextInt():\t\t" + r3.nextInt());
        System.out.println("r3.nextDouble():\t" + r3.nextDouble());
        System.out.println("r3.nextGaussian():\t" + r3.nextGaussian());
    }
}

运行上面程序,看到如下结果:

第一个种子为50的Random对象
r1.nextBoolean():    true
r1.nextInt():        -1727040520
r1.nextDouble():    0.6141579720626675
r1.nextGaussian():    2.377650302287946
---------------------------
第二个种子为50的Random对象
r2.nextBoolean():    true
r2.nextInt():        -1727040520
r2.nextDouble():    0.6141579720626675
r2.nextGaussian():    2.377650302287946
---------------------------
种子为100的Random对象
r3.nextBoolean():    true
r3.nextInt():        -1139614796
r3.nextDouble():    0.19497605734770518
r3.nextGaussian():    0.6762208162903859

从上面运行结果来看,只要两个 Random 对象的种子相同,而且方法的调用顺序也相同,它们就会产生相同的数字序列。也就是说,Random 产生的数字并不是真正随机的,而是一种伪随机。

为了避免两个 Random 对象产生相同的数字序列,通常推荐使用当前时间作为 Random 对象的种子,如下代码所示。

Random rand = new Random(System.currentTimeMillis());

在多线程环境下使用 ThreadLocalRandom 的方式与使用 Random 基本类似,如下程序片段示范了 ThreadLocalRandom 的用法。

ThreadLocalRandom rand = ThreadLocalRandom.current();
// 生成一个4~20之间的伪随机整数
int val1 = rand.nextInt(4,20);
// 生成一个2.0~10.0之间的伪随机浮点数
double val2 = rand.nextDouble(2.0,10.0);

BigDecimal 类

float、double 两个基本类型的浮点数容易引起精度丢失。先看如下程序。

public class DoubleTest {
    public static void main(String args[]) {
        System.out.println("0.05 + 0.01 = " + (0.05 + 0.01));
        System.out.println("1.0 - 0.42 = " + (1.0 - 0.42));
        System.out.println("4.015 * 100 = " + (4.015 * 100));
        System.out.println("123.3 / 100 = " + (123.3 / 100));
    }
}

程序输出的结果是:

0.05 + 0.01 = 0.060000000000000005
1.0 - 0.42 = 0.5800000000000001
4.015 * 100 = 401.49999999999994
123.3 / 100 = 1.2329999999999999

上面程序运行结果表明,Java 的 double 类型会发生精度丢失,尤其在进行算术运算时更容易发生这种情况。不仅是 Java,很多编程语言也存在这样的问题。

为了能精确表示、计算浮点数,Java 提供了 BigDecimal 类,该类提供了大量的构造器用于创建 BigDecimal 对象,包括把所有的基本数值型变量转换成一个 BigDecimal 对象,也包括利用数字字符串、数字字符数组来创建 BigDecimal 对象。

查看 BigDecimal 类的 BigDecimal(double val) 构造器的详细说明时,可以看到不推荐使用该构造器的说明,主要是因为使用该构造器时有一定的不可预知性。当程序使用 new BigDecimal(0.1) 来创建一个BigDecimal 对象时,它的值并不是0.1,它实际上等于一个近似0.1的数。这是因为0.1无法准确地表示为 double 浮点数,所以传入 BigDecimal 构造器的值不会正好等于0.1(虽然表面上等于该值)。

如果使用 BigDecimal(String val) 构造器的结果是可预知的————写入new BigDecimal("O.1") 将创建一个 BigDecimal,它正好等于预期的0.1,因此通常建议优先使用基于 String 的构造器。

如果必须使用 double 浮点数作为 BigDecimal 构造器的参数时,不要直接将该 double 浮点数作为构造器参数创建 BigDecimal 对象,而是应该通过 BigDecimal.valueOf(double value) 静态方法来创建BigDecimal 对象。

BigDecimal 类提供了add()、subtract()、multiply()、divide()、pow() 等方法对精确浮点数进行常规算术运算。下面程序示范了 BigDecimal 的基本运算。

public class BigDecimalTest {
    public static void main(String[] args) {
        BigDecimal f1 = new BigDecimal("0.05");
        BigDecimal f2 = BigDecimal.valueOf(0.01);
        BigDecimal f3 = new BigDecimal(0.05);
        System.out.println("使用String作为BigDecimal构造器参数:");
        System.out.println("0.05 + 0.01 = " + f1.add(f2));
        System.out.println("0.05 - 0.01 = " + f1.subtract(f2));
        System.out.println("0.05 * 0.01 = " + f1.multiply(f2));
        System.out.println("0.05 / 0.01 = " + f1.divide(f2));
        System.out.println("使用double作为BigDecimal构造器参数:");
        System.out.println("0.05 + 0.01 = " + f3.add(f2));
        System.out.println("0.05 - 0.01 = " + f3.subtract(f2));
        System.out.println("0.05 * 0.01 = " + f3.multiply(f2));
        System.out.println("0.05 / 0.01 = " + f3.divide(f2));
    }
}

上面程序中 fl 和 f3 都是基于0.05创建的 BigDecimal 对象,其中 fl 是基于"0.05"字符串,但是基于0.05的 double 浮点数。运行上面程序,看到如下运行结果:

使用String作为BigDecimal构造器参数:
0.05 + 0.01 = 0.06
0.05 - 0.01 = 0.04
0.05 * 0.01 = 0.0005
0.05 / 0.01 = 5
使用double作为BigDecimal构造器参数:
0.05 + 0.01 = 0.06000000000000000277555756156289135105907917022705078125
0.05 - 0.01 = 0.04000000000000000277555756156289135105907917022705078125
0.05 * 0.01 = 0.0005000000000000000277555756156289135105907917022705078125
0.05 / 0.01 = 5.000000000000000277555756156289135105907917022705078125

从上面运行结果可以看出进行算术运算的效果,而且可以看出创建 BigDecimal 对象时,一定要使用 String 对象作为构造器参数,而不是直接使用 double 数字。

注意:创建 BigDecimal 对象时,不要直接使用 double 浮点数作为构造器参数来调用 BigDecimal 构造器,否则同样会发生精度丢失的问题。

如果程序中要求对 double 浮点数进行加、减、乘、除基本运算,则需要先将 double 类型数值包装成 BigDecimal 对象,调用 BigDecimal 对象的方法执行运算后再将结果转换成 double 型变量。这是比较烦琐的过程,可以考虑以 BigDecimal 为基础定义一个 Arith 工具类,该工具类代码如下。

public class Arith {
    // 默认除法运算精度
    private static final int DEF_DIV_SCALE = 10;

    // 构造器私有,让这个类不能实例化
    private Arith() {
    }

    // 提供精确的加法运算。
    public static double add(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.add(b2).doubleValue();
    }

    // 提供精确的减法运算。
    public static double sub(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.subtract(b2).doubleValue();
    }

    // 提供精确的乘法运算。
    public static double mul(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.multiply(b2).doubleValue();
    }

    // 提供(相对)精确的除法运算,当发生除不尽的情况时.
    // 精确到小数点以后10位的数字四舍五入。
    public static double div(double v1, double v2) {
        BigDecimal b1 = BigDecimal.valueOf(v1);
        BigDecimal b2 = BigDecimal.valueOf(v2);
        return b1.divide(b2, DEF_DIV_SCALE, RoundingMode.HALF_UP).doubleValue();
    }

    public static void main(String[] args) {
        System.out.println("0.05 + 0.01 = " + Arith.add(0.05, 0.01));
        System.out.println("1.0 - 0.42 = " + Arith.sub(1.0, 0.42));
        System.out.println("4.015 * 100 = " + Arith.mul(4.015, 100));
        System.out.println("123.3 / 100 = " + Arith.div(123.3, 100));
    }
}

Arith 工具类还提供了 main 方法用于测试加、减、乘、除等运算。运行上面程序将看到如下运行结果:

0.05 + 0.01 = 0.06
1.0 - 0.42 = 0.58
4.015 * 100 = 401.5
123.3 / 100 = 1.233

上面的运行结果才是期望的结果,这也正是使用 BigDecimal 类的作用。

posted @ 2020-02-07 16:49  认真对待世界的小白  阅读(436)  评论(0编辑  收藏  举报