不要再使用double,float来计算精度值了,BigDecimal才是最好用的

大家好,我是程序员大猩猩。

在我们敲代码的时候,有时候会碰到一些莫名其妙的问题。而且是在很简单的俩个浮点数运算的情况下发生。发生错误后,我们脑袋里是不是会有一个大大的问号,心里还在默念,这怎么可能?

我们来看看以下代码,为什么运行结果和我们的想法一致呢?

public static void main(String[] args) {
    System.out.println(0.11D + 3001299.32D);
    System.out.println(0.11F + 4001299.32F);
}

如上,代码中就是俩个简单的Double和Float的浮点数相加,我们甚至不用运行,就可算出它俩的答案。

第一个答案:3001299.43 另一个答案是: 4001299.43

但是,我们来运行一下这个main方法,看看它的运行结果?

嗯?怎么不对呢?为什么?

因为在 Java 中,double 和 float 类型使用的是二进制浮点数表示法,这种表示法在表示某些十进制小数时可能会出现无限循环或近似值,从而导致精度损失。例如:0.1 在二进制表示中是一个无限循环小数,因此在 double 或 float 类型中无法精确表示。

那么我们该如何避免出现这样的错误呢? 这里就进入我们要说的BigDecimal了,它在Java中不是一个基础类型,而是一个类。

那么为什么BigDecimal可以保证精度计算,不丢精度值呢?

BigDecimal它基于整数或字符串表示的数学上的十进制数,可以精确地表示任何十进制数。

BigDecimal 内部使用一个 int 类型的 scale 来表示小数点后的位数,和一个 BigInteger 类型的 unscaledValue 来表示去掉小数点后的整数部分。这种表示方法允许 BigDecimal 在进行算术运算时保持精确的十进制精度。

// BigDecimal源码
public class BigDecimal extends Number implements Comparable<BigDecimal> {
  /**
     * The unscaled value of this BigDecimal, as returned by {@link
     * #unscaledValue}.
     *
     * @serial
     * @see #unscaledValue
     */
  private final BigInteger intVal;
  
  /**
     * The scale of this BigDecimal, as returned by {@link #scale}.
     *
     * @serial
     * @see #scale
     */
    private final int scale;  // Note: this may have any value, so
                              // calculations must be done in longs
}

当我们使用 BigDecimal 进行加、减、乘、除等运算时,它会在内部进行精确的数学运算,确保结果尽可能精确。在进行除法运算时,由于可能产生除不尽的情况,BigDecimal 允许指定舍入模式(如四舍五入、向上取整、向下取整等)来处理这种情况。

BigDecimal的运算性能比基础运算方式要慢,为什么呢?

a. 因为BigDecimal是一个类,它的产生还包含BigInteger和其他元数据。

b. 既然它是一个类,那么它的创建和销毁都要消耗虚拟机资源。

c. BigDecimal属于十进制的运算,它为了使结果精确精度值的准确度,势必需要更多的运算时间。而基础运算方式是二进制的,它是在电脑硬件上的数据计算,肯定快。

那么我们为什么还要用BigDecimal来计算精度值呢?

因为它损耗的性能,完全可以弥补我们在基础运算方式上无法获取准确精度的错误。第二个还有BigDecimal内还包含很多方法,可以使我们能自由的选择我们所需的浮点数保留的浮点精度。

如何使用BigDecimal?

BigDecimal方法内包含add subtract multiply divide 加减乘除四种基础操作,且包含setScale精度控制的方法。

public static void main(String[] args) {
    BigDecimal a = new BigDecimal("3001299.32");
    BigDecimal b = new BigDecimal("0.11");

    // 加法
    BigDecimal add = a.add(b);
    System.out.println("加法结果:" + add);

    // 减法
    BigDecimal subtract = a.subtract(b);
    System.out.println("减法结果:" + subtract);

    // 乘法
    BigDecimal multiply = a.multiply(b);
    System.out.println("乘法结果:" + multiply);

    // 除法并使结果结果保留两位小数,四舍五入
    BigDecimal divide = a.divide(b, 2, RoundingMode.HALF_UP);
    System.out.println("除法结果:" + divide);
}

当然,在使用BigDecimal时,一定要注意以下错误情况

一、使用错误的传参或者构造函数

// 源码
public BigDecimal(double val) {
    this(val,MathContext.UNLIMITED);
}

以上传入double类型的源码,我们尽量少用,因为double本身就有精度问题,那么我们使用传入double的BigDecimal构造函数,可能会导致精度损失,我们建议使用字符串或者整数构造BigDecimal对象,如下。

BigDecimal a = new BigDecimal("3001299.32");

二、BigDecimal在使用除法时,一定要默认保留精度。

BigDecimal divide = a.divide(b, 2, RoundingMode.HALF_UP);

不用使用以下代码,可能会报错的。

BigDecimal divide = a.divide(b);

三、不要使用equals来比较俩个BigDecimal,请使用compareTo

// 错误的, 永远是false
if(a.equals(b)){

}
// 正确使用
if(a.compareTo(b) < 0){
    System.out.println("a小于b");
}
if(a.compareTo(b) == 0){
    System.out.println("a等于b");
}
if(a.compareTo(b) > 0){
    System.out.println("a大于b");
}
if(a.compareTo(b) > -1){
    System.out.println("a大于等于b");
}
if(a.compareTo(b) < 1){
    System.out.println("a小于等于b");
}如上代码,正确使用大小比较的方法。

如上,这里介绍了BigDecimal的使用和常见错误问题识别和正确使用方法,再见。