BigDecimal的使用全面总结

BigDecimal

BigDecimal可以表示任意大小,任意精度的有符号十进制数。所以不用怕精度问题,也不用怕大小问题,放心使用就行了。就是要注意的是,使用的时候有一些注意点。还有就是要注意避免创建的时候存在精度问题,尤其是不要使用new BigDecimal(double val)这个构造方法来创建BigDecimal对象,因为double本来就不精确。

使用场景

BigDecimal,相信对于很多人来说都不陌生,很多人都知道他的用法,这是一种java.math包中提供的一种可以用来进行精确运算的类型。

很多人都知道,在进行金额表示、金额计算等场景,不能使用double、float等类型,而是要使用对精度支持的更好的BigDecimal。

所以,很多支付、电商、金融等业务中,BigDecimal的使用非常频繁。

注意点

  1. 使用BigDecimal的equals方法并不能验证两个数是否真的相等
  2. BigDecimal的使用的第一步就是创建一个BigDecimal对象,如果创建对象的时候没有正确创建,那么后面怎么算都是错的!

如何正确的创建一个BigDecimal?

关于这个问题,在《阿里巴巴Java开发手册》中有一条建议,或者说是要求:

BigDecimal的使用全面总结_第1张图片

这是一条【强制】建议,那么,这背后的原理是什么呢?

想要搞清楚这个问题,主要需要弄清楚以下几个问题:

1、为什么说double不精确?

2、BigDecimal是如何保证精确的?

在知道这两个问题的答案之后,我们也就大概知道为什么不能使用BigDecimal(double)来创建一个BigDecimal了。

double、float为什么不精确?

首先,计算机是只认识二进制的,即0和1,这个大家一定都知道。

那么,所有数字,包括整数和小数,想要在计算机中存储和展示,都需要转成二进制。

十进制整数转成二进制很简单,通常采用"除2取余,逆序排列"即可,如10的二进制为1010。

但是,小数的二进制如何表示呢?

十进制小数转成二进制,一般采用"乘2取整,顺序排列"方法,如0.625转成二进制的表示为0.101。

但是,并不是所有小数都能转成二进制,如0.1就不能直接用二进制表示,他的二进制是0.000110011001100… 这是一个无限循环小数。

所以,计算机是没办法用二进制精确的表示0.1的。也就是说,在计算机中,很多小数没办法精确的使用二进制表示出来。

那么,这个问题总要解决吧。那么,人们想出了一种采用一定精度的近似值表示一个小数的办法。这就是IEEE 754(IEEE二进制浮点数算术标准)规范的主要思想。

IEEE 754规定了多种表示浮点数的表示方式,其中最常用的就是32位单精度浮点数和64位双精度浮点数。

在Java中,就用了这个思想。java中使用float和double分别用来表示单精度浮点数和双精度浮点数,但是注意哈,java中的小数实际上还是用"乘2取整,顺序排列"组成的0.000110011001100… 这种01序列来保存的哈。只是他是这样的,比如你有一个double类型的0.1,那么java允许你用0.1000000000000000055511151231257827021181583404541015625这样的,一个能被01表示的、又近似等于0.1的数代替0.1在计算机中进行存储,因为0.1用01序列保存是无限位的,但是这个0.1000000000000000055511151231257827021181583404541015625就可以被有限个01序列表示,而且保证如果只看前17位,那么这两个值是一样大的,这就是double的精度保证至少是17位是准确的效果。

float和double的精度不同,可以简单的理解为保留有效位数不同。

  1. float的精度是保证至少8位有效数字是准确的。
  2. double的精度是保证至少17位有效数字是准确的。

float和double的精度如下:

序号 数据类型 描述 有效位
1 float 单精度 8位
2 double 双精度 17位

其实,java保存一个小数,是采用保留有效位数的一个近似小数,作为这个小数实际存储的值的。

比如:

double d=0.1;

其实d的值被计算机保存的是0.1000000000000000055511151231257827021181583404541015625。他只保证前17位的数字是正确的,剩余的他就不保证了。

测试1:

BigDecimal的使用全面总结_第2张图片

注意:执行toString只会打印有效位数的数据。比如,你打印double的数据,就一定只打印前17位数字(不记小数点,就看纯数字),你打印float的数据,一定打印前8位数字。而且toString你看到的结果最后一位是被进位的。

BigDecimal的使用全面总结_第3张图片

所以,大家也就知道为什么double和float表示的小数不精确了。

BigDecimal如何精确计数?

BigDecimal的解决方案就是,不使用二进制,而是使用十进制(BigInteger)+小数点位置(scale)来表示小数。

比如下面这个代码:

public static void main(String[] args) {
	BigDecimal bd = new BigDecimal("100.001");
	System.out.println(bd.scale());
	System.out.println(bd.unscaledValue());
}

输出:

BigDecimal的使用全面总结_第4张图片

也就是100.001 = 100001 * 0.1^3。这种表示方式下,避免了小数的出现,当然也就不会有精度问题了。十进制,也就是整数部分使用了BigInteger来表示,小数点位置只需要一个整数scale来表示就OK了。这个100001 就是无标度值,3是标度值。

如果大家看过BigDecimal的源码,其实可以发现,实际上一个BigDecimal就是通过一个"无标度值"和一个"标度"来表示一个数的。

BigDecimal类中的属性主要包括下面五个:

BigDecimal的使用全面总结_第5张图片

在BigDecimal中,标度是通过scale字段来表示的。

而无标度值的表示比较复杂。

我们看三个场景:

情景一:
package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal b1 = new BigDecimal("3.1415926");
    }
}

BigDecimal的使用全面总结_第6张图片

这个stringCache是一个String的类型,intCompact是long类型的,precision是int类型的,scale也是int类型的:

BigDecimal的使用全面总结_第7张图片

从Debug的结果看,intVal为空,因为当无标度值没有超过Long.MAX_VALUE,无标度值会被压缩存储到intCompact中,precision表示有8个数字位(不算小数点),scale表示标度为7(表示无标度值把小数点向左移动7位就是实际值)。

即,无标度值没有超过Long.MAX_VALUE(即,9223372036854775807),intVal为空,并且会用intCompact存储无标度值,相当于是压缩了空间,intCompact用的空间更少。

情景二:
package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal b2 = new BigDecimal("31415926314159263141592631415926");
    }
}

BigDecimal的使用全面总结_第8张图片

当无标度值超过Long.MAX_VALUE(即,9223372036854775807),才会用intVal记录无标度值,而intCompact里面就只存一个Long.MIN_VALUE就行了。上面的precision表示当前数字位为32个,scale为0表示没有小数位。

情景三:
package com.liudashuai;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

public class Test {
    public static void main(String[] args) {
        MathContext mc3 = new MathContext(30,RoundingMode.HALF_UP);
        BigDecimal b2 = new BigDecimal("31415926314159263141592631415926",mc3);
    }
}

BigDecimal的使用全面总结_第9张图片

在这里我们手动设置了precision为30,所以最后两位被丢弃并执行了舍入操作,同时scale记录为-2,这个-2表示无标度值(可以看到上面无标度的值intVal没有看最后的26)表示的数的小数点向右移动两位是实际值。

RoundingMode.HALF_UP表示:四舍五入。不设置的话,默认五舍六入。

你看,不知定RoundingMode.HALF_UP,只指定precision为30,默认如果第31个数是6,那么是进位的。

BigDecimal的使用全面总结_第10张图片

总结

通过上面三个例子我们对BigDecimal的5个基本属性总结如下:

  1. BigDecimal是通过unscaled value和scale来构造,同时使用Long.MAX_VALUE作为我们是否压缩的阈值。
  2. 当unscaled value超过阈值时采用intVal字段存储unscaled value,intCompact字段存储Long.MIN_VALUE。
  3. 如果unscaled value没有超过阈值,对unscaled value进行压缩存储到long型的intCompact字段,并用于后续计算,而intVal字段则为空。
  4. scale字段存储标度,可以理解为unscaled value最后一位到实际值小数点的距离。如例1中对于3.1415926来说unscaled value为31415926,最后一位6到实际值的小数点距离为7,scale记为7;对于例3中手动设置precision的情况,unscaled value为31415926xxx159的最后一位9到实际值31415926xxx15900的小数点距离为2,由于在小数点左边scale则记为-2。
  5. precision字段记录的是unscaled value的数字个数,当手动指定MathContext并且指定的precision小于实际precision的时候,会要求进行rounding操作。

关于无标度值的压缩机制大家了解即可,不是本文的重点,大家只需要知道BigDecimal主要是通过一个无标度值和标度来表示的就行了。

BigDecimal的构造方法

我们都知道,想要创建一个对象,需要使用该类的构造方法,在BigDecimal中有很多个构造方法。

一般记下面这四个就行了:

public class BigDecimal extends Number implements Comparable<BigDecimal> {
	public BigDecimal(int val) {
        this.intCompact = val;
        this.scale = 0;
        this.intVal = null;
    }
	public BigDecimal(double val) {
        this(val,MathContext.UNLIMITED);
    }
	public BigDecimal(String val) {
        this(val.toCharArray(), 0, val.length());
    }
    public BigDecimal(long val) {
        this.intCompact = val;
        this.intVal = (val == INFLATED) ? INFLATED_BIGINT : null;
        this.scale = 0;
    }
    ……
}

以上四个方法,创建出来的的BigDecimal的标度(scale)是不同的。

其中 BigDecimal(int)和BigDecimal(long) 比较简单,因为都是整型,所以他们的标度都是0。

而BigDecimal(double) 和BigDecimal(String)的标度就有很多学问了。

BigDecimal中提供了一个通过double创建BigDecimal的方法——BigDecimal(double) ,但是,同时也给我们留了一个坑!

因为我们知道,double表示的小数是不精确的,如0.1这个数字,double只能表示他的近似值。

所以,当我们使用new BigDecimal(0.1)创建一个BigDecimal 的时候,其实创建出来的值并不是正好等于0.1的。

而是0.1000000000000000055511151231257827021181583404541015625。这是因为doule自身表示的只是一个近似值。

BigDecimal的使用全面总结_第11张图片

所以,如果我们在代码中,使用BigDecimal(double) 来创建一个BigDecimal的话,那么是损失了精度的,这是极其严重的。

那么,该如何创建一个精确的BigDecimal来表示小数呢,答案是使用String创建。

而对于BigDecimal(String) ,当我们使用new BigDecimal(“0.1”)创建一个BigDecimal 的时候,其实创建出来的值正好就是等于0.1的。他的标度是1。是准确地表示0.1这个数的。

BigDecimal的比较

需要注意的是,new BigDecimal(“0.10000”)和new BigDecimal(“0.1”)这两个数的标度分别是5和1,所以如果你使用BigDecimal的equals方法进行比较的话,得到的结果会是false。

例子1:

BigDecimal的使用全面总结_第12张图片

例子2:

package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal bigDecimal = new BigDecimal(1);
        BigDecimal bigDecimal1 = new BigDecimal(1);
        System.out.println(bigDecimal.equals(bigDecimal1));

        BigDecimal bigDecimal2 = new BigDecimal(1);
        BigDecimal bigDecimal3 = new BigDecimal(1.0);
        System.out.println(bigDecimal2.equals(bigDecimal3));

        BigDecimal bigDecimal4 = new BigDecimal("1");
        BigDecimal bigDecimal5 = new BigDecimal("1.0");
        System.out.println(bigDecimal4.equals(bigDecimal5));
    }
}

BigDecimal的使用全面总结_第13张图片

通过以上代码示例,我们发现,在使用BigDecimalequals方法对1和1.0进行比较的时候,有的时候是 true(当使用 int、double 定义 BigDecimal 时),有的时候是 false(当使用 String 定义 BigDecimal时)。

那么,为什么会出现这样的情况呢,我们先来看下BigDecimalequals方法。

BigDecimal对应的代码如下:

BigDecimal的使用全面总结_第14张图片

看到它比较的时候,其实还比较了精度。还会比较一些其他的东西。所以,值是一样的,使用equals方法得到的结果可能会判断他们两个不是相等的。

上面看到bigDecimal2和bigDecimal3相等、bigDecimal和bigDecimal1相等,是因为他们的精度也是一样的。

BigDecimal的使用全面总结_第15张图片

但是,可能精度一样,值也一样,使用equals方法判断可能也不相等。我们看到源码里面还有其他一些判断,我没看懂,防止,使用equals方法判断两个BigDecimal对象是否相等的变数比较多,结果不好预测。所以不建议使用equals方法比较两个BigDecimal对象是否相等,如果要比较的话,建议使用compareTo方法。

如下:

使用compareTo:

package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal bigDecimal=new BigDecimal("1.2");
        BigDecimal bigDecimal2=new BigDecimal("1.20");
        int rs = bigDecimal.compareTo(bigDecimal2);
        System.out.println(rs);

        BigDecimal bigDecimal3=new BigDecimal("1.21");
        BigDecimal bigDecimal4=new BigDecimal("1.20");
        int rs1 = bigDecimal3.compareTo(bigDecimal4);
        System.out.println(rs1);

        BigDecimal bigDecimal5=new BigDecimal("0.9");
        BigDecimal bigDecimal6=new BigDecimal("1.20");
        int rs2 = bigDecimal5.compareTo(bigDecimal6);
        System.out.println(rs2);

        /*
        rs = -1,表示bigdemical小于bigdemical2;
        rs = 0,表示bigdemical等于bigdemical2;
        rs = 1,表示bigdemical大于bigdemical2;
        */
    }
}

BigDecimal的使用全面总结_第16张图片

BigDecimal的valueOf方法
package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal bigDecimal = BigDecimal.valueOf(12345679.1);//结果为12345679.1
        System.out.println(bigDecimal);
    }
}

这个是能精准表示12345679.1的,因为他的底层如下:

BigDecimal的使用全面总结_第17张图片

相当于是把小数变为了字符串,然后去创建BigDecimal对象,所以是精准的。

但是注意:

BigDecimal的使用全面总结_第18张图片

上面的情况,只表示了小数点后14位的数,所以实际这个bigDecimal变量里面保存的数值为:123.12345678901235。因为Double.toString(123.12345678901235)返回的字符串只保留17位数。所以只能精准保存了123.12345678901235这个值。

如果要表示位数很多的一个小数,建议还是使用new BigDecimal(String)来创建对象:

BigDecimal的使用全面总结_第19张图片

BigDecimal常用方法

注意:BigDecimal进行运算时必须要保证对象本身不能是null,否则就会抛空指针异常。

方法 含义
add(BigDecimal) BigDecimal对象中的值相加,返回BigDecimal对象
subtract(BigDecimal) BigDecimal对象中的值相减,返回BigDecimal对象
multiply(BigDecimal) BigDecimal对象中的值相乘,返回BigDecimal对象
divide(BigDecimal) BigDecimal对象中的值相除,返回BigDecimal对象。该方法可能会遇到无限精度问题,会抛出异常,使用时需注意。详细见下方的"无限精度的坑"
abs() 将BigDecimal对象中的值转换成绝对值
doubleValue() 将BigDecimal对象中的值转换成双精度数
floatValue() 将BigDecimal对象中的值转换成单精度数
longValue() 将BigDecimal对象中的值转换成长整数
intValue() 将BigDecimal对象中的值转换成整数
compareTo(BigDecimal val) 比较大小,返回int类型。0(相等) 1(大于) -1(小于)
toString() 有必要时使用科学计数法。
toPlainString() 不使用任何指数。推荐使用
toEngineeringString() 有必要时使用工程计数法。 工程记数法是一种工程计算中经常使用的记录数字的方法,与科学技术法类似,但要求10的幂必须是3的倍数
max(BigDecimal val) 两值比较,返回最大值
negate() 求相反数,正变负,负变正
pow(int n) 求乘方,如BigDecimal.valueOf(2).pow(3)的值为8

代码示例:

package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal b1 = new BigDecimal("1.1");
        BigDecimal b2 = new BigDecimal("2.5");
        BigDecimal b3 = new BigDecimal("4");
        BigDecimal b4 = new BigDecimal("-5.53");
        System.out.println("相加:"+b1.add(b2));
        System.out.println("相减:"+b1.subtract(b2));
        System.out.println("相乘:"+b2.multiply(b3));
        System.out.println("相除:"+b2.divide(b3));
        System.out.println("绝对值:"+b4.abs());

        //有时候我们进行精确运算后,还需要转为对应的类型,我们可以用下面这些API
        double v = b1.doubleValue();
        float f = b1.floatValue();
        long l = b1.longValue();
        int i = b1.intValue();

        System.out.println(v);
        System.out.println(f);
        System.out.println(l);
        System.out.println(i);
        System.out.println(new BigDecimal("1.9").intValue());//输出1,可以看出把小数的BigDecimal转为整数会直接丢掉小数,只看整数部分。

        System.out.println("1.1和-5.53中较大的值为:"+b4.max(b1));
        System.out.println("-5.53的相反数是:"+b4.negate());

        System.out.println("4的三次方是:"+b3.pow(3));
    }
}

BigDecimal的使用全面总结_第20张图片

上面三个返回值字符串的方法如下:

  • toPlainString() : 不使用任何指数。
  • toString() :有必要时使用科学计数法。
  • toEngineeringString():有必要时使用工程计数法。 工程记数法是一种工程计算中经常使用的记录数字的方法,与科学技术法类似,但要求10的幂必须是3的倍数
打印值 不使用指数-toPlainString() 科学记数法-toString() 工程记数法-toEngineeringString()
0.0001 0.0001 0.0001 0.0001
0.0000001 0.0000001 1E-7 100E-9

注意:toString()、toEngineeringString()方法在某些时候会使用科学计数法或工程计数法,但是不是所有情况都会使用科学计数法或工程计数法的

对于一个数值可能很小的BigDecimal对象来说,使用toString()可能由于打印的数值太小而打印其科学计数法表示,而使用toPlainString()才能打印完整的数值。

package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal bg = new BigDecimal("1E11");
        System.out.println(bg.toString()); // 1E+11
        System.out.println(bg.toPlainString()); // 100000000000
        System.out.println(bg.toEngineeringString()); // 100E+9

        BigDecimal b = new BigDecimal("100000000000");
        System.out.println(b.toString()); // 1E+11
        System.out.println(b.toPlainString()); // 100000000000
        System.out.println(b.toEngineeringString()); // 100E+9
    }
}

BigDecimal的使用全面总结_第21张图片

那么toString什么时候使用科学计数法呢?

源码中toString方法上有很长的注释,主要介绍指数计算转换过程,简要总结了两种toString()方法会以科学计数方式输出的场景。

先解释一下:

unscaledValue :整数非标度值 (即去掉小数点的Bigdecimal的值,类型为BigInteger)
scale:标度值,如果为零或正数,则标度是小数点后的位数。如果为负数,则将该数的非标度值乘以 10 的负 scale 次幂。
如下,BigDecimal值为123.00,则unscaledValue 为12300,而scale为2,套用公式,则数值是相等的。

场景一:scale为负数,一定会转换为科学计数的方式

BigDecimal的使用全面总结_第22张图片

场景二:
需要先计算变动指数的值。公式为-scale+(unscaleValue.length-1) ,如果该值小于-6,那么则会使用科学计数的方式输出字符串。

package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        //案例一
        BigDecimal b1 = new BigDecimal("0.000000123").setScale(9);
        System.out.println(b1.toString());
        System.out.println(b1.toPlainString());
        System.out.println(b1.scale());
        System.out.println(b1.unscaledValue());
        System.out.println("=======");
        /*
        输出结果:
            1.23E-7
            0.000000123
            9
            123
        */


        //案例二
        BigDecimal b2 = new BigDecimal("0.000001234").setScale(9);
        System.out.println(b2.toString());
        System.out.println(b2.toPlainString());
        System.out.println(b2.scale());
        System.out.println(b2.unscaledValue());
        System.out.println("=======");
        /*
        输出结果:
            0.000001234
            0.000001234
            9
            1234
        */

        //案例三
        BigDecimal b3 = new BigDecimal("0.123000000").setScale(9);
        System.out.println(b3.toString());
        System.out.println(b3.toPlainString());
        System.out.println(b3.scale());
        System.out.println(b3.unscaledValue());
        System.out.println("=======");
        /*
        输出结果:
            0.123000000
            0.123000000
            9
            123000000
        */

        //案例四
        BigDecimal b4 = new BigDecimal("123000000");
        System.out.println(b4.toString());
        System.out.println(b4.toPlainString());
        System.out.println(b4.scale());
        System.out.println(b4.unscaledValue());
        System.out.println("=======");
        /*
        输出结果:
            123000000
            123000000
            0
            123000000
        */

        //案例五
        //Double d = 12345678d; Double d = 12345678.0; 效果一样
        Double d = (double) 12345678;
        BigDecimal b5 = BigDecimal.valueOf(d);
        System.out.println(d);
        System.out.println(b5.toString());
        System.out.println(b5.toPlainString());
        System.out.println(b5.scale());
        System.out.println(b5.unscaledValue());
        System.out.println("=======");
        /*
        输出结果:
            1.2345678E7
            12345678
            12345678
            0
            12345678
        */

    }
}

BigDecimal的使用全面总结_第23张图片

注意,这个unscaleValue、scale与BigDecimal是否压缩无关。所以toString是否以科学计数法来展示和是否压缩也无关,就看-scale+(unscaleValue.length-1)<-6是否成立,成立就展示科学计数法。

比如:

package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        //这个值是会被压缩的,但是不会unscaledValue()的值,unscaledValue()的值还是不看小数点的值,即31415926
        BigDecimal b1 = new BigDecimal("0.000000226");
        System.out.println(b1.scale());
        System.out.println(b1.unscaledValue());
        System.out.println(b1.toString());
        //下面这个值不会被压缩的,unscaledValue()还是不看小数点的值,即31415926314159263141592631415926
        BigDecimal b2 = new BigDecimal("0.00000000031415926314159263141592631415926");
        System.out.println(b2.scale());
        System.out.println(b2.unscaledValue());
        System.out.println(b2.toString());
    }
}

BigDecimal的使用全面总结_第24张图片

toEngineeringString()与工程计数法:

如果一个BigDecimal对象执行toString()是以指数形式返回,那么调用toEngineeringString()则以工程计数法返回,只是要注意,工程计数法返回的10的幂必须是3的倍数就行了。

除法要注意的事情

使用divide除法函数除不尽,出现无线循环小数的时候,会出现报错。报错为:Exception in thread “main” java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("10");
        BigDecimal b = new BigDecimal("3");
        System.out.println(a.divide(b));
    }
}

BigDecimal的使用全面总结_第25张图片

注意:如果你直接对一个数约定保留位数也一样会有上面的问题。

package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal b = new BigDecimal("1.6666");
        System.out.println("result b:" + b.setScale(2, BigDecimal.ROUND_HALF_UP)); // 1.67
        System.out.println("result b:" + b.setScale(2)); // 精度错误
    }
}

BigDecimal的使用全面总结_第26张图片

BigDecimal的提供了八种舍入模式,我们可以用这个来解决上面的问题(下面例子都是假设保留0位小数哈。):

1.ROUND_UP:

舍入时尽量远离0,有值就进行进位操作。舍入为一位时如下表:

处理前 处理后
1.1 2
2.8 3
1.0 1
-1.3 -2
-2.9 -3
-1.0 -1
1.x 2
-1.x -2

总结:无论正负,上一位数字加1(处理数非0时)

2.ROUND_DOWN

舍入时尽量接近0,有值就进行进位操作。舍入为一位时如下表:

处理前 处理后
1.1 1
2.8 2
1.0 1
-1.3 -1
-2.9 -2
-1.0 -1
1.x 1
-1.x -1

总结:无论正负,上一位数字不变

3.ROUND_CEILING

舍入时接近正无穷方向,即向数字大的方向,有值时进行进位操作。舍入为一位时如下表:

处理前 处理后
1.1 2
2.8 3
1.0 1
-1.3 -1
-2.9 -2
-1.0 -1
1.x 2
-1.x -1

总结:正数时,与 ROUND_UP 相同,远离0,上一位数字加1(处理数非0时);负数时,与ROUND_DOWN相同,靠近0,上一位数字不变

4.ROUND_FLOOR

舍入时接近负无穷方向,即向数字小的方向,有值时进行进位操作。舍入为一位时如下表:

处理前 处理后
1.1 1
2.8 2
1.0 1
-1.3 -2
-2.9 -3
-1.0 -1
1.x 1
-1.x -2

总结:正数时,与ROUND_DOWN相同,靠近0,上一位数字不变;负数时,与 ROUND_UP 相同,远离0,上一位数字加1(处理数非0时)

5.ROUND_HALF_UP

最经典的四舍五入。向最接近的数字方向进行取舍,若为5(两边一样近),则向上取舍。舍入为一位时如下表:

处理前 处理后
1.1 1
2.8 3
1.0 1
1.5 2
-1.3 -1
-2.9 -3
-1.0 -1
-1.5 -2

总结:当处理位大于等于5时,与 ROUND_UP 相同,远离0,上一位数字加1;当处理位小于5时,与ROUND_DOWN相同,靠近0,上一位数字不变

6.ROUND_HALF_DOWN

五舍六入,类似于四舍五入,向最接近的数字方向进行取舍。但与此不同的是,若为5(两边一样近),则向下取舍。舍入为一位时如下表:

处理前 处理后
1.1 1
2.8 3
1.0 1
1.5 1
-1.3 -1
-2.9 -3
-1.0 -1
-1.5 -1

总结:与四舍五入类似。当处理位大于5时,与 ROUND_UP 相同,远离0,上一位数字加1;当处理位小于等于5时,与ROUND_DOWN相同,靠近0,上一位数字不变

7.ROUND_HALF_EVEN

银行家舍入法,四舍六入五成双(五分两种情况)。向最接近的数字方向进行取舍,若为5,根据5后面的数字来确定,当5后有数时,舍5进1;当5后无有效数字时,需要分两种情况来讲:

(1)5前一位为奇数,舍5进1

(2)5前一位为偶数,舍5不进

如舍入为一位时下表举例:

处理前 处理后
1.1 1
2.8 3
1.0 1
1.5 2
2.5 2
2.51 3
-1.3 -1
-2.9 -3
-1.0 -1
-1.5 -2
-2.5 -2
-2.51 -3

总结:四舍六入,当处理位大于5时,与 ROUND_UP 相同,远离0,上一位数字加1;当处理位小于5时,与ROUND_DOWN相同,靠近0,上一位数字不变;当处理位等于5时,分情况进行取舍

8.ROUND_UNNECESSARY

进行断言,要有精确的结果,不进行舍入。如果不精确,会抛出ArithmeticException,如下表举例:

处理前 处理后
1.1 ArithmeticException
2.8 ArithmeticException
1.0 1
1.5 ArithmeticException
-1.3 ArithmeticException
-2.9 ArithmeticException
-1.0 -1
-1.5 ArithmeticException
1.502 ArithmeticException
1.000 1

总结:只有精确时输出数字,其余不精确时,抛出ArithmeticException。总之就是,要求保留的位数之后,若还有数据,那么就报错,没有数据就不报错,保留的位数之后是无意义的0也不报错。

这些舍入模式,可以在下面两个方法中指定:

  • public BigDecimal setScale(int newScale, RoundingMode roundingMode)
    • int newScale 精确小数位
    • RoundingMode roundingMode 舍入模式
  • public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
    • BigDecimal divisor 除数
    • int scale 精确小数位
    • int roundingMode 舍入模式

第二种方式适合在除的时候指定精确位数和舍入模式。

例子1:(保留0位小数,我们测试一下上面ROUND_UP舍入模式的第一个例子)

package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal b = new BigDecimal("1.1");
        //保留两位小数,0位小数之后的数使用BigDecimal.ROUND_UP进行舍入
        System.out.println("result b:" + b.setScale(0, BigDecimal.ROUND_UP));

        
    }
}

BigDecimal的使用全面总结_第27张图片

例子2:(保留多位小数)

package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal b = new BigDecimal("1.1431");
        //保留两位小数,2位小数之后的数使用BigDecimal.ROUND_UP进行舍入
        System.out.println("result b:" + b.setScale(2, BigDecimal.ROUND_UP));


    }
}

BigDecimal的使用全面总结_第28张图片

例子3:

package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal b = new BigDecimal("1.6666");
        System.out.println("result b:" + b.setScale(2, BigDecimal.ROUND_HALF_UP)); // 1.67 四舍五入
        System.out.println("result b:" + b.setScale(2, BigDecimal.ROUND_DOWN)); // 1.66 向靠近0反向舍入
        System.out.println("result b:" + b.setScale(2, BigDecimal.ROUND_UP)); // 1.67 向远离0反向舍入
    }
}

BigDecimal的使用全面总结_第29张图片

还要注意一点:

package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal b = new BigDecimal("1.6666");
        System.out.println("result b:" + b.setScale(2, BigDecimal.ROUND_HALF_UP)); // 1.67
        System.out.println("result b:" + b.setScale(2)); // 精度错误
    }
}

BigDecimal的使用全面总结_第30张图片

setScale方法默认使用的roundingMode是ROUND_UNNECESSARY。这时你设置保留2位小数,但是小数点后有4位,所以会抛异常。这就是前面例子里抛出异常的原因。

例子4:

使用public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode),除的时候,设置舍入规则,又同时设置保留的小数位数。这样就不怕除不尽了抛出异常了。

package com.liudashuai;

import java.math.BigDecimal;

public class Test {
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("10");
        BigDecimal b = new BigDecimal("3");
        System.out.println(a.divide(b,2,BigDecimal.ROUND_DOWN));
    }
}

BigDecimal的使用全面总结_第31张图片

格式化展示BigDecimal值

这里不展开介绍DecimalFormat类的使用,我们就记几个常用的就行了。这个DecimalFormat主要是用于格式输出数值用的一个类。这个类的用法可以自己百度。

这里举一个“千位符格式展示价格”的例子:

package com.liudashuai;

import java.math.BigDecimal;
import java.text.DecimalFormat;

public class Test {
    public static void main(String[] args) {

        BigDecimal p = new BigDecimal("1299792458.124456");
        BigDecimal p2 = new BigDecimal("1299792458.1260");
        BigDecimal p3 = new BigDecimal("1299792458.1251");
        BigDecimal p4 = new BigDecimal("1299792458.1250");
        BigDecimal p5 = new BigDecimal("1299792458.1350");
        //每三位以逗号进行分隔。
        System.out.println(new DecimalFormat(",###.00").format(p));//1,299,792,458.12
        //每三位以逗号进行分隔。
        System.out.println(new DecimalFormat(",###.00").format(p2));//1,299,792,458.13
        //每三位以逗号进行分隔。
        System.out.println(new DecimalFormat(",###.00").format(p3));//1,299,792,458.13
        //每三位以逗号进行分隔。
        System.out.println(new DecimalFormat(",###.00").format(p4));//1,299,792,458.12
        //每三位以逗号进行分隔。
        System.out.println(new DecimalFormat(",###.00").format(p5));//1,299,792,458.14
    }
}

BigDecimal的使用全面总结_第32张图片

负数一样可以哈:

BigDecimal的使用全面总结_第33张图片

依然是银行家算法:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一

下面再举一个例子:

package com.liudashuai;

import java.math.BigDecimal;
import java.text.DecimalFormat;

public class Test {
    public static void main(String[] args) {
        DecimalFormat df = new DecimalFormat();
        String style=null;

        BigDecimal data = new BigDecimal("1299792458.124465");

        // 在格式后添加单位字符
        style = "00000.000 kg";
        df.applyPattern(style);
        System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));

        // 模式中的"%"表示乘以100并显示为百分数,要放在最后。
        style = "0.00 %";
        df.applyPattern(style);
        System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));

        // 模式中的"\u2030"表示乘以1000并显示为千分数,要放在最后。
        style = "0.00 \u2030";
        DecimalFormat df1 = new DecimalFormat(style);  //在构造函数中设置数字格式
        System.out.println("采用style: " + style + "  格式化之后: " + df1.format(data));

        // 嵌入文本中
        style = "这件衣服的价格是 ##.00 元";
        df.applyPattern(style);
        System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));

        // 嵌入文本中
        style = "这件衣服的价格是 ,###.00 元";
        df.applyPattern(style);
        System.out.println("采用style: " + style + "  格式化之后: " + df.format(data));
    }
}

BigDecimal的使用全面总结_第34张图片

踩坑
  1. BigDecimal.valueOf(double)方法,如果你传的double位数不多,那么是创建的BigDecimal表示的数是精确表示你指定的数的,但是如果你传的double位数多,创建的BigDecimal可能就不精确表示你指定的数了,具体看下面例子,所以建议使用new BigDecimal(String)方法创建对象。虽然,阿里规约里面说可以使用BigDecimal.valueOf(double),但是我觉得不建议使用。

    注意:如果你调用BigDecimal的valueOf方法,传的是一个float变量,那么实际执行的还是BigDecimal.valueOf(double)方法,相当于是double变量接受一个float变量而已,是ok的。这里有一点,你传一个float变量的时候,你传一个位数少的float数,创建的BigDecimal也不是精确表示你指定的数的,具体如下。

    package com.liudashuai;
    
    import java.math.BigDecimal;
    
    public class Test {
        public static void main(String[] args) {
            double a=31.123456789123456789;
            System.out.println(Double.toString(a));//31.123456789123455
            System.out.println(BigDecimal.valueOf(a));//31.123456789123455
            System.out.println(BigDecimal.valueOf(a).add(new BigDecimal("0.000000000000004")));//实际结果应该是31.123456789123460,但是显示31.123456789123459,因为31.123456789123456789表示为BigDecimal为31.123456789123455,精度丢失了。
    
            double b=3.35;
            System.out.println(Double.toString(b));//3.35
            System.out.println(BigDecimal.valueOf(b)); //3.35。位数少时,BigDecimal.valueOf(double)精确。
    
            float c = 3.33f;
            System.out.println(Double.toString(c));//3.3299999237060547
            System.out.println(BigDecimal.valueOf(c)); //3.3299999237060547。位数少时,BigDecimal.valueOf(float)也不精确。
        }
    }
    

    BigDecimal的使用全面总结_第35张图片

    public static BigDecimal valueOf(double val) 方法的源码如下:

    BigDecimal的使用全面总结_第36张图片

    其实就相当于是new BigDecimal(String),只是他是把你传过来的数先执行Double.toString(……),然后把返回值作为new BigDecimal(String)的参数去创建对象的,如果你执行Double.toString(……)方法没有你想要的精确值,那么创建的BigDecimal也不会精确,这就是上面展示这个效果的原因了。所以还是建议直接使用new BigDecimal(String)创建BigDecimal对象。

  2. 等值比较的坑

    一般在比较两个值是否相等时,都是用equals 方法,但是,在BigDecimal 中使用equals可能会导致结果错误,BigDecimal 中提供了 compareTo 方法,在很多时候需要使用compareTo 比较两个值。如下所示:

    BigDecimal的使用全面总结_第37张图片

    所以,要比较两个BigDecimal值的大小,尽量采用compareTo方法;

  3. 无限精度的坑

    BigDecimal 并不代表无限精度,当在两个数除不尽的时候,就会出现无限精度的坑,如下所示:

    BigDecimal的使用全面总结_第38张图片

    在官方文档中对该异常有如下说明:

    If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result, an ArithmeticException is thrown. Otherwise, the exact result of the division is returned, as done for other operations.

    大致意思就是,如果在除法(divide)运算过程中,如果商是一个无限小数(如 0.333…),而操作的结果预期是一个精确的数字,那么将会抛出ArithmeticException异常。

    此种情况,只需要在使用 divide方法时指定结果的精度和舍入模式即可:

    BigDecimal的使用全面总结_第39张图片

  4. BigDecimal三种字符串输出的坑

    BigDecimal的使用全面总结_第40张图片

    可以看到三种方式输出的结果可能都不相同,可能这个并不是预期的结果 ,BigDecimal 有三个方法可以转为相应的字符串类型,切记不要用错:

    以下内容介绍java.math.BigDecimal下的三个toString方法的区别及用法

    toPlainString() : 不使用任何指数。
    toString() :有必要时使用科学计数法。
    toEngineeringString():有必要时使用工程计数法。 工程记数法是一种工程计算中经常使用的记录数字的方法,与科学技术法类似,但要求10的幂必须是3的倍数

  5. 使用BigDecimal进行计算时参数不能为NULL

    BigDecimal的使用全面总结_第41张图片

  6. 使用BigDecimal进行除法计算时被除数不能为0

    BigDecimal的使用全面总结_第42张图片

  7. 除不尽的我们必须指定取舍模式和精确位数,这样得到的值就不是那个实际的值了,所以计算可能会出错,这个要自己注意一下。

    BigDecimal的使用全面总结_第43张图片

工具类
package com.liudashuai;

import java.math.BigDecimal;

/**
 * 用于高精确处理常用的数学运算
 */
public class ArithmeticUtils {
    //默认除法运算精度
    private static final int DEF_DIV_SCALE = 10;

    /**
     * 提供精确的加法运算
     *
     * @param v1 被加数
     * @param v2 加数
     * @return 两个参数的和
     */

    public static double add(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.add(b2).doubleValue();
    }

    /**
     * 提供精确的加法运算
     *
     * @param v1 被加数
     * @param v2 加数
     * @return 两个参数的和
     */
    public static BigDecimal add(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.add(b2);
    }

    /**
     * 提供精确的加法运算
     *
     * @param v1    被加数
     * @param v2    加数
     * @param scale 保留scale 位小数
     * @return 两个参数的和
     */
    public static String add(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.add(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 提供精确的减法运算
     *
     * @param v1 被减数
     * @param v2 减数
     * @return 两个参数的差
     */
    public static double sub(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.subtract(b2).doubleValue();
    }

    /**
     * 提供精确的减法运算。
     *
     * @param v1 被减数
     * @param v2 减数
     * @return 两个参数的差
     */
    public static BigDecimal sub(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.subtract(b2);
    }

    /**
     * 提供精确的减法运算
     *
     * @param v1    被减数
     * @param v2    减数
     * @param scale 保留scale 位小数
     * @return 两个参数的差
     */
    public static String sub(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.subtract(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 提供精确的乘法运算
     *
     * @param v1 被乘数
     * @param v2 乘数
     * @return 两个参数的积
     */
    public static double mul(double v1, double v2) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.multiply(b2).doubleValue();
    }

    /**
     * 提供精确的乘法运算
     *
     * @param v1 被乘数
     * @param v2 乘数
     * @return 两个参数的积
     */
    public static BigDecimal mul(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.multiply(b2);
    }

    /**
     * 提供精确的乘法运算
     *
     * @param v1    被乘数
     * @param v2    乘数
     * @param scale 保留scale 位小数
     * @return 两个参数的积
     */
    public static double mul(double v1, double v2, int scale) {
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return round(b1.multiply(b2).doubleValue(), scale);
    }

    /**
     * 提供精确的乘法运算
     *
     * @param v1    被乘数
     * @param v2    乘数
     * @param scale 保留scale 位小数
     * @return 两个参数的积
     */
    public static String mul(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.multiply(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 提供(相对)精确的除法运算,当发生除不尽的情况时,精确到
     * 小数点以后10位,以后的数字四舍五入
     *
     * @param v1 被除数
     * @param v2 除数
     * @return 两个参数的商
     */

    public static double div(double v1, double v2) {
        return div(v1, v2, DEF_DIV_SCALE);
    }

    /**
     * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
     * 定精度,以后的数字四舍五入
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 表示表示需要精确到小数点以后几位。
     * @return 两个参数的商
     */
    public static double div(double v1, double v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException("The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(Double.toString(v1));
        BigDecimal b2 = new BigDecimal(Double.toString(v2));
        return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    }

    /**
     * 提供(相对)精确的除法运算。当发生除不尽的情况时,由scale参数指
     * 定精度,以后的数字四舍五入
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 表示需要精确到小数点以后几位
     * @return 两个参数的商
     */
    public static String div(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException("The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v1);
        return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 提供精确的小数位四舍五入处理
     *
     * @param v     需要四舍五入的数字
     * @param scale 小数点后保留几位
     * @return 四舍五入后的结果
     */
    public static double round(double v, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException("The scale must be a positive integer or zero");
        }
        BigDecimal b = new BigDecimal(Double.toString(v));
        return b.setScale(scale, BigDecimal.ROUND_HALF_UP).doubleValue();
    }

    /**
     * 提供精确的小数位四舍五入处理
     *
     * @param v     需要四舍五入的数字
     * @param scale 小数点后保留几位
     * @return 四舍五入后的结果
     */
    public static String round(String v, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b = new BigDecimal(v);
        return b.setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 取余数
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 小数点后保留几位
     * @return 余数
     */
    public static String remainder(String v1, String v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        return b1.remainder(b2).setScale(scale, BigDecimal.ROUND_HALF_UP).toString();
    }

    /**
     * 取余数  BigDecimal
     *
     * @param v1    被除数
     * @param v2    除数
     * @param scale 小数点后保留几位
     * @return 余数
     */
    public static BigDecimal remainder(BigDecimal v1, BigDecimal v2, int scale) {
        if (scale < 0) {
            throw new IllegalArgumentException(
                    "The scale must be a positive integer or zero");
        }
        return v1.remainder(v2).setScale(scale, BigDecimal.ROUND_HALF_UP);
    }

    /**
     * 比较大小
     *
     * @param v1 被比较数
     * @param v2 比较数
     * @return 如果v1 大于v2 则 返回true 否则false
     */
    public static boolean compare(String v1, String v2) {
        BigDecimal b1 = new BigDecimal(v1);
        BigDecimal b2 = new BigDecimal(v2);
        int bj = b1.compareTo(b2);
        boolean res;
        if (bj > 0) {
            res = true;
        } else {
            res = false;
        }
        return res;
    }
}

总结

因为计算机采用二进制处理数据,但是很多小数,如0.1的二进制是一个无线循环小数,而这种数字在计算机中是无法精确表示的。

所以,人们采用了一种通过近似值的方式在计算机中表示,于是就有了单精度浮点数和双精度浮点数等。

所以,作为单精度浮点数的float和双精度浮点数的double,在表示小数的时候只是近似值,并不是真实值。

所以,当使用BigDecimal(Double)创建一个的时候,得到的BigDecimal是损失了精度的。

想要避免这个问题,可以通过BigDecimal(String)的方式创建BigDecimal,这样的情况下,0.1就会被精确的表示出来。其表现形式是一个无标度数值1,和一个标度1的组合。

当然这里我们还需要注意一下BigDecimal的一些坑哈。要掌握BigDecimal的API的使用。

注意:

  1. 在需要精确的小数计算时再使用BigDecimal,BigDecimal的性能比double和float差,在处理庞大,复杂的运算时尤为明显。故一般精度的计算没必要使用BigDecimal。
  2. 尽量使用参数类型为String的构造函数。
  3. BigDecimal都是不可变的, 在进行每一次四则运算时,都会产生一个新的对象 ,老对象不知道计算后的值,做加减乘除运算后,结果是在新对象中的。

你可能感兴趣的:(python,开发语言)