文章目录
一、引入
我们先来看一段代码,程序结果如下
System.out.println(0.09 + 0.01); // 0.09999999999999999
System.out.println(0.216 - 0.1); // 0.11599999999999999
System.out.println(0.226 * 0.01); // 0.0022600000000000003
System.out.println(0.09 / 0.1); // 0.8999999999999999
可以发现得到的结果和我们想要得到的结果不一样!这是为什么呢?
想要知道为什么计算结果不精确,首先我们就需要来看一看在计算机中小数是如何存的
二、计算机中的小数
在计算机中,不管是运算,还是存储,都是转为二进制再进行操作。
假设现在有一个十进制的数据:69.875
,它转成二进制是什么样的呢?
它会有两部分:
- 整数部分的二进制:
0100 0101
- 小数部分的二进制:
111
![image-20240422124219127](https://img-blog.csdnimg.cn/img_convert/7d81d502e78e0edabd1bc946a1c6b529.png)
为什么是这样的呢?
我们可以把二进制再转回十进制来看一看。
小数部分转十进制的方式和整数部分是一样的,都是乘以 基数的权次幂
, 然后再相加。
要注意的是:整数部分的权是 0、1、2、3、4...
这样依次递增的;而小数部分这里的权是 -1、-2、-3
这样依次递减的。
因此知道了,我们刚刚转为的二进制数是没有任何问题的。
但是由于现在我们研究的是小数部分的不精确性,因此现在只看小数部分的二进制。
0.875
是一个比较特殊的数字,小数部分用 111
就能表示了。
![image-20240422124718926](https://img-blog.csdnimg.cn/img_convert/1b3afcbc88d7fcd48e175dbbc23073fc.png)
但假如此时我换一个数字:0.9
,此时它小数部分的二进制就非常非常长了,需要使用 45位
才能够表示。
如果再换一个数字 0.226
,它小数部分的二进制就更长了,需要用 55位
才能表示。
由此可见,如果我们把十进制的小数转成二进制,那么它的结果有可能会很长很长很长很长
而在Java中,float
和 double
在底层占用的字节数是有限的。
如果超出了小数部分bit位数,就会进行舍弃。
![image-20240422125055171](https://img-blog.csdnimg.cn/img_convert/5c61b7dd9477ec477a9f6823436349ad.png)
我们用这个理论去看上面的 0.226
,它的小数部分一共有 55位
,如果我用 double
去记录它,前面的 52
位我可以留着,但是后面还有三位,就只能舍弃了。
因此小数在存储的时候,有可能就是不精确的。
当我用一个不精确的数据去参与计算,不管是 加减乘除
,结果都有可能是不精确的。
但是在实际开发中如果你的数据不精确,大多数情况下可能问题不大。
但是有些特殊的场景它的问题就很大了,它是需要我们进行精确运算的。
例如在银行里就是这样的:假设以后我们买房子,贷款 100万
,按照 4.9%
的利息还 30年
,计算出每个月要还的钱,结果就是右边的图。
这个时候就需要精确运算了,而且小数点需要保留后面的两位。
除此之外,在飞机、火箭这些精密零件上,也必须要有精确运算。
所以说在实际开发中,像一些金融、证券、精密零件计算… 都需要用到小数点精确运算。
为了解决这个问题,Java就提供了一个类:BigDecimal
,这个类就可以帮助我们进行小数的精确运算。
三、BigDecimal
的作用
-
用于小数的精确计算
-
用来表示很大的小数
这个特点跟
BigInteger
是一样的
那么 BigDecimal
这个类该如何用呢?我们可以到 API帮助文档
中查看一下。
四、阅读 API帮助文档
查看API文档,我们可以看到API文档中关于BigDecimal类的定义如下:
首先看它的包,它是在 java.math包
下的。
![image-20240422130220683](https://img-blog.csdnimg.cn/img_convert/ac92d1a485a020416e38092dd6769428.png)
再往下,就是它的继承结构:它的父类是 Number
,爷爷是 Object
再往下,去看它的一些描述:BigDecimal
是不可变的、任意精度的有符号十进制数。
看 不可变
,这个特点跟 BigInteger
的特点是一样的,一旦创建对象之后,不管是做 加减乘除
等操作,它原本的值就不会再变了,而是显示一个新的 BigDecimal对象
;并且 任意精度
可知,精度
也可以得到保证。
再往下,我们就要去看它的构造方法。
可以发现它有很多很多的构造,你可以根据不同的情况去选择不同的构造,在这里我会跟大家介绍几个构造。
下面这个构造是将一个小数变成 BigDecimal
对象,我们用鼠标点击一下这个蓝色的。
就可以看到这个构造方法的介绍。
将 double
转换为 BigDecimal
,后者是 double
的二进制浮点值准确的十进制表示形式。
但是在这,它会有一个小小的注意点:此构造方法的结果有一定的不可预知性。
也就是说如果你用这种方式去创建一个 BigDecimal
对象的话,它的结果还有可能是不精确的。
那怎么办?我需要精确运算。
如果你想要精确运算,那就需要用到下面这个构造:跟 BigInteger
一样,它还是通过一个字符串的表示形式将它变成 BigDecimal
的对象,我们用鼠标点击一下它,它就会跳转到详情界面。
往下翻,这里它也有一个注意点:它不会遇到 BigDecimal(double)
构造方法的不可预知问题。
也就是说我们用这种方式去计算的话,它的结果就是精确的。
五、构造方法代码示例
1)BigDecimal(double)
首先,通过传递double类型的小数来创建对象
BigDecimal bd1 = new BigDecimal(0.01);
BigDecimal bd2 = new BigDecimal(0.09);
将 bd1
和 bd2
分别来做一个打印
System.out.println(bd1);
System.out.println(bd2);
程序运行结果如下,都不用我们去进行 加减乘除
运算,结果已经就不精确了。
![image-20240422131533560](https://img-blog.csdnimg.cn/img_convert/f1df5c7f62196ea96971502e62bcb4bd.png)
因此,这种方式有可能是不精确的,所以不建议使用。因此接下来说第二个,建议大家使用的。
2)BigDecimal(String)
通过传递字符串表示的小数来创建对象
BigDecimal bd3 = new BigDecimal("0.01");
BigDecimal bd4 = new BigDecimal("0.09");
System.out.println(bd3);
System.out.println(bd4);
程序运行完毕,可以看见数据是精确的。
![image-20240422131900134](https://img-blog.csdnimg.cn/img_convert/ce6d40e33623d9dbc63eb758c7113bd8.png)
并且我们可以来运算一下,看产生的新的 BigDecimal
对象 结果是否准确
BigDecimal bd3 = new BigDecimal("0.01");
BigDecimal bd4 = new BigDecimal("0.09");
BigDecimal bd5 = bd3.add(bd4);
System.out.println(bd3);
System.out.println(bd4);
System.out.println(bd5);
程序运行完毕,可以看见,结果也是精确的
![image-20240422132033548](https://img-blog.csdnimg.cn/img_convert/a37ef26ac699e1b1ee464af9683ffbf8.png)
我们反过来来看看 一、引入
中写的案例:小数直接运算,它的结果就是不精确的。
![image-20240422132127844](https://img-blog.csdnimg.cn/img_convert/678e791d06086a43ce163d649485e1ed.png)
但是我现在使用 BigDecimal
运算,它的结果就是精确的,这个就是 BigDecimal
最大的特点。
3)通过静态方法 valueOf(double val)
获取对象
BigDecimal
跟 BigInteger
一样,在 BigDecimal类
中,它也有一个静态方法:valueOf(double val)
,可以帮助我们去获取一个 BigDecimal
的对象。
public static BigDecimal valueOf(double val)
我们可以在静态方法中传递整数也可以传递小数,如果传递整数,例如 10
,它实际上是以 10.0
的方式参与计算的。
BigDecimal bd6 = BigDecimal.valueOf(10);
System.out.println(bd6); // 10
4)2) 和 3)
的区别
① 如果要表示的数字不大,没要超出 double
的取值范围,建议使用静态方法
② 如果要表示的数字比较大,超出了 double
的取值范围,建议使用构造方法
③ 如果我们传递的是0~10之间的整数,包含0,包含10,那么方法会返回已经创建好的对象,不会重新new
value(double val)
在底层其实也做了一些小小的判断,我们简单的来阅读一下源码
5)查看源码
选中 BigDecimal
,ctrl + b 跟进,ctrl + F12 搜一下 valueOf
,我们先带着大家去看参数是 double
类型的这个方法。
![image-20240422133401770](https://img-blog.csdnimg.cn/img_convert/8baf40505c442d22756d6f0389b16312.png)
这个方法的原码非常的简单,它是直接把传递过来的数字,变成字符串,然后再传递给构造方法创建对象就行了。
因此它底层其实也是 new
出来的。
![image-20240422133506140](https://img-blog.csdnimg.cn/img_convert/4850b6433c747f165193ab3770cd7255.png)
另一个 valueOf
方法我们也来看一下,这次我们要找的是参数是 long类型
的。
![image-20240422133624157](https://img-blog.csdnimg.cn/img_convert/be93f6299c5d5f77d1cdaa2e6fd50fe1.png)
这个方法的底层稍微来讲就复杂很多。
![image-20240422133703552](https://img-blog.csdnimg.cn/img_convert/8b317652a787f980509ea812d14983a9.png)
下面我们来逐行分析一下
// 首先拿着参数val来做一个判断:如果在 [0, ZERO_THROUGH_TEN.length) 之间
if (val >= 0 && val < ZERO_THROUGH_TEN.length)
我们先来看一下 ZERO_THROUGH_TEN.length
是什么,选中 ZERO_THROUGH_TEN
ctrl + b 。
可以发现它就是一个数组,在数组里面它提前创建好了很多 BigDecimal
的对象。
例如:第一个 BigDecimal
表示的是 0
,第二个 BigDecimal
表示的是 1
…
由此可见,它其中把 0 ~ 10
都已经创建好了对象,数组的长度是 11
。
![image-20240422134023036](https://img-blog.csdnimg.cn/img_convert/082cc98e3fafb3c014738c172072b0f2.png)
回到上一步,继续分析 valueOf(long val)
的源代码
public static BigDecimal valueOf(long val) {
// 如果你传递过来的整数在 [0, 11) 之间,就从数组中去拿已经创建好了的对象返回数据
if (val >= 0 && val < ZERO_THROUGH_TEN.length)
return ZERO_THROUGH_TEN[(int)val];
// 如果是其他情况,都是 new 出来的
else if (val != INFLATED)
return new BigDecimal(null, val, 0, 0);
return new BigDecimal(INFLATED_BIGINT, val, 0, 0);
}
这样做的目的其实跟 BigInteger
是一样的,也是为了节约内存,将经常使用的数据已经提前准备好了,你下次要用的时候直接给你就好了,不会重新创建。
我们也可以用代码来做一个验证,运行结果为 true
,表示 b6
跟 b7
其实是同一个对象。
BigDecimal bd6 = BigDecimal.valueOf(10);
BigDecimal bd7 = BigDecimal.valueOf(10);
System.out.println(bd6 == bd7); // true
但如果我传入的参数是 10.0
,结果还会是 true
吗?显然,结果为 false
BigDecimal bd6 = BigDecimal.valueOf(10.0);
BigDecimal bd7 = BigDecimal.valueOf(10.0);
System.out.println(bd6 == bd7);
因为在底层,如果你的参数是一个 double
类型的小数,都是 new
出来的。
如果你传递的是0~10之间的整数,包含0,包含10,那么方法才会返回已经创建好的对象,不会重新new
六、常见的成员方法
它的成员方法跟 BigInteger
是类似的
public static BigDecimal valueOf(double val) // 获取对象
public BigDecimal add(BigDecimal value) // 加法
public BigDecimal subtract(BigDecimal value) // 减法
public BigDecimal multiply(BigDecimal value) // 乘法
public BigDecimal divide(BigDecimal value) // 除法
除法它有两个重载的,还有一个除法我们可以设置:精确几位(小数点后你要保留几位)
、舍入模式(进一法 / 去尾法 / 四舍五入)
public BigDecimal divide(BigDecimal val, 精确几位, 舍入模式) // 除法
七、成员方法代码实现
1)加法
BigDecimal bd1 = BigDecimal.valueOf(10.0);
BigDecimal bd2 = BigDecimal.valueOf(2.0);
BigDecimal bd3 = bd1.add(bd2);
System.out.println(bd3); // 12.0
2)减法
BigDecimal bd4 = bd1.subtract(bd2);
System.out.println(bd4); // 8.0
3)乘法
BigDecimal bd5 = bd1.multiply(bd2);
System.out.println(bd5);//20.00
4)除法
BigDecimal bd6 = bd1.divide(bd2);
System.out.println(bd6); // 5
上面的除法,运算完后是一个整数,结果是精确的。
但如果,我运算完后,它是一个小数怎么办?
程序运行完毕,发现结果也是没有问题的
BigDecimal bd1 = BigDecimal.valueOf(10.0);
BigDecimal bd2 = BigDecimal.valueOf(4.0);
BigDecimal bd6 = bd1.divide(bd2);
System.out.println(bd6); // 2.5
上面两个都是可以除尽的情况,但如果除不尽会怎么样?
BigDecimal bd1 = BigDecimal.valueOf(10.0);
BigDecimal bd2 = BigDecimal.valueOf(3.0);
BigDecimal bd6 = bd1.divide(bd2);
System.out.println(bd6); // 2.5
运行程序进行测试,控制台输出结果如下所示:在 main方法的32行
报错
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
at java.base/java.math.BigDecimal.divide(BigDecimal.java:1716)
at com.itheima.api.bigdecimal.demo02.BigDecimalDemo02.main(BigDecimalDemo02.java:14)
针对这个问题怎么解决,此时我们就需要使用到BigDecimal类中另外一个divide方法,如下所示:
BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
上述divide方法参数说明:
divisor: 除数对应的BigDecimal对象
scale: 精确的位数,小数点后面要保留几位
roundingMode: 取舍模式,最为常用的就是四舍五入
在以前,我们会这么写:ROUND_HALF_UP
就表示 四舍五入
BigDecimal bd6 = bd1.divide(bd2, 2, BigDecimal.ROUND_HALF_UP);
System.out.println(bd6); // 3.33 —— 小数点后面保留两位小数,舍去的部分用四舍五入
但是细心的同学发现,这个常量上面有一个横线,出现这种标记,就表示你的代码已经过时。
![image-20240422140303244](https://img-blog.csdnimg.cn/img_convert/f0827b8750e32a5fd51ba659e7e953d4.png)
JDK9的时候这种使用 BigDecimal中的常量
的写法已经过时了
![image-20240422140518587](https://img-blog.csdnimg.cn/img_convert/4ae9cf8d837269f341000a18bc8e8b2d.png)
Java 觉得这些东西是一些计算的保留方式,如果直接将它放到 BigDecimal
中感觉不太好,因此Java就将这些 计算的摄入模式
,单独的写到一个类中,这个类叫:RoundingMode
。
![image-20240422140837539](https://img-blog.csdnimg.cn/img_convert/738800a752d8835a237b40bd9417344f.png)
在这个枚举类中定义了很多种取舍方式。
这里用 四舍五入
举例,程序运行完毕,可以发现还是 3.33
BigDecimal bd6 = bd1.divide(bd2, 2, RoundingMode.HALF_UP);
System.out.println(bd6); // 3.33
八、舍入模式
在Java中它其实给我们规定了很多舍入模式,打开一下文档来看一下
找到成员方法 divide()
,参数为 RoundingMode
的重载方法,用鼠标点击一下这里的 RoundingMode
RoundingMode
是一个枚举,枚举我们还没有学习,现在我们可以把它也理解成是一个类,它里面用到的这些东西,就可以把它理解为:用 public static final
所修饰的常量,后面会有一个单独的章节去讲解这个枚举的。
此时就可以看见,在Java中给我们规定了这么多的舍入模式
![image-20240422141331585](https://img-blog.csdnimg.cn/img_convert/21bd1058d28c771ad27639672b673b7a.png)
往下拉,它对每一种舍入模式都做了一个详解,这个解释光用嘴巴说说不清楚,我们需要通过数轴的方式来理解。
1)UP
左右一边画图一边对比API文档。
UP
:远离零方向舍入的舍入模式。
什么叫做 远离零
?
针对于正数来讲,向右是远离零。针对负数来讲,向左是远离零,这个就是 UP
。
在下面它会有一些举例:如果你是用 5.5
采用 UP
这种舍入模式的话,远离零
那不就是 6
吗。
我们来看个负数:-1.1
采用 UP
这种舍入模式的话,远离零
那不就是 -2
吗。
2)DOWN
DOWN
:向零方向舍入的舍入模式。
可以发现跟上面的 UP
刚好是反过来的。
针对整数来说,向零方向
应该是向左。
针对于负数来说,向零方向
应该是向右。
看右边的举例:如果你是用 5.5
采用 DOWN
这种舍入模式的话,向零方向
那不就是 5
吗。
我们来看个负数:-1.6
采用 DOWN
这种舍入模式的话,向零方向
那不就是 -1
吗。
3)CEILING
CEILING
:向正无限大方向舍入的舍入模式。
正无限大
跟我们以前在数学中说的 正无穷大
是一样的。
向正无限大方向
这个就很简单了,不管你是正数,还是负数,都是向右的方向。
看右边的举例:如果你是用 5.5
采用 CEILING
这种舍入模式的话,向正无限大方向
那不就是 6
吗。
![image-20240422142907117](https://img-blog.csdnimg.cn/img_convert/de5e646e1dcc4ca436ce119c3659408c.png)
再来看一个负数,如果你是用 -2.5
采用 CEILING
这种舍入模式的话,向正无限大方向
那不就是 -2
吗。
![image-20240422142943485](https://img-blog.csdnimg.cn/img_convert/d967c1ea65001007f5712393b68a3cc6.png)
4)FLOOR
FLOOR
:向负无限大方向舍入的舍入模式。
FLOOR
的结果跟 CEILING
就是反过来的。
向负无限大方向
不管你是正数,还是负数,都是向左的方向。
看右边的举例:如果你是用 5.5
采用 FLOOR
这种舍入模式的话,向负无限大方向
那不就是 5
吗。
![image-20240422143150650](https://img-blog.csdnimg.cn/img_convert/0acf19c8a4cd759ab6cfa24207b3ce73.png)
再来看一个负数,如果你是用 -5.5
采用 CEILING
这种舍入模式的话,向负无限大方向
那不就是 -6
吗。
![image-20240422143309707](https://img-blog.csdnimg.cn/img_convert/a3f3f83954a13e70ce4cf32db6a90dbe.png)
5)HALE_UP
这个就是我们用到最多的 四舍五入
。
HALF_UP
:此舍入模式就是通常学校里讲的四舍五入。
在前面也有它的说明:向最接近数字方向舍入的舍入模式,如果与两个相邻数字的距离相等(例如 0.5
跟 1
和 2
距离都相等),则向上舍入(进一)。如果被舍弃部分 >= 0.5,则舍入行为同 RoundingMode.UP
(进一);否则舍入行为同 RoundingMode.DOWN
(舍去)。
6)HALF_DOWN
跟 HALE_UP
四舍五入所类似的还有一个:HALF_DOWN
,这两种方式唯一的区别就是:在数字等于 0.5
的时候是不一样的。
HALF_DOWN
:如果与两个相邻数字的距离相等,则向下舍入。
这些舍入模式不需要大家去背,只要大家记住 四舍五入:HALE_UP
即可。
但如果我们在实际开发中用到了其他的舍入模式,学会找就行了。
先找 BigDecimal类
——> 找到除法 divide()
,这个方法里面需要我们指定舍入模式 ——> 点击 RoundingMode
,然后找你需要的舍入模式就行了。
但一般来说,没有什么特殊的需求,一般我们用的都是 HALE_UP
。
九、扩展
1)引出结论
我们要来看一下 BigDecimal
在计算机中到底是怎么存储的,它跟 BigInteger
还不太一样。
在这里我还是以 0.226
为例,它的小数部分变成二进制后有 55位
。
![image-20240422145051319](https://img-blog.csdnimg.cn/img_convert/bcd15a7d2ad4dce40c0762ec79874bd2.png)
如果我们是按照之前的 BigInteger
的方式,每三十二位去分成一组进行计算的话,这样其实是有一些小弊端的。
0.226
的小数位置其实还不够长,如果我换一个小数,它的二进制更长,有几百位,几千位怎么办?那这种分段进行存储的方式效率就非常的低,因此在Java中,BigDecimal
采取了另外一种存储方式。
我们在获取 BigDecimal
对象的时候,不管你是用 valueOf()
获取,还是 new
通过构造方法获取,其实最终它都是 new
出来的,参数都是一个字符串形式的小数。
![image-20240422145318361](https://img-blog.csdnimg.cn/img_convert/b16a91d28b7cf7b33c69df18692d968c.png)
Java拿到这个字符串后,它会做这样的事情:遍历,然后得到里面的每一个字符,然后再把这些字符转换成 ASCII码表
中对应的数值再来进行存储。
所以说 BigDecimal
在底层其实也是一个数组,数组里存的是 每一个字符
在 ASCII码表
中所对应的数字。
倘若我现在把数字变一变,变成:123.226
,它就是以一个长度为 7
的数组进行存储的。
那如果我现在存的是一个负数,负数在前面还有一个负号,此时在数组当中,它就会把负号所对应的 ASCII码表值
存过来。
PS:如果你存的是正数,它是不会将负号存进去的。
2)验证
我们来到IDEA中采用 Debug模式
验证一下。
将代码赋值到测试类中,右键 Debug
BigDecimal bd1 = BigDecimal.valueOf(0.226);
BigDecimal bd2 = BigDecimal.valueOf(123.226);
BigDecimal bd3 = BigDecimal.valueOf(-1.5);
一直点击下一步,直接让断点走完这三行
![image-20240422150040007](https://img-blog.csdnimg.cn/img_convert/9e8b031162d7949d63b012ddc1cc71ae.png)
先来看 bd1
里面,可以看见它是一个 byte类型
的数组,长度是 5
,数组中的数据为:[48, 46, 50, 50, 54]
。
跟我们刚刚推断的完全一模一样。
![image-20240422150413448](https://img-blog.csdnimg.cn/img_convert/cb85353c072b2e2ad89d1e038159c9b6.png)
再来看 bd2
,它里面也是一个 byte
类型的数组,长度是 7
,里面的内容是 [49, 50, 51, 46, 50, 50, 54]
跟我们刚刚推断的也是一模一样的。
再来看 bd3
,它里面存储的也是一个 byte
类型的数组,长度是 4
,里面的内容是 [45, 49, 46, 53]
。
其中 45
表示的是负号,49
是 1
,46
是 .
,53
是 5
。
那 BigDecimal
有上限吗?
十、BigDecimal
的上限
答案是:有的。
在Java中数组的长度是有上线的:int
的最大值。
因此你要记录的小数,它的总长度超过了 int的最大值
,那么 BigDecimal
还是记录不了的。
但之前我们又说了,你的电脑内存是扛不住这么大的数的,因此我们可以把 BigDecimal
认为是无限的。
十一、总结
1、BigDecimal
的作用是什么?
-
表示较大的小数
-
解决小数运算精度失真的问题
简单理解:就是可以让小数进行精确运算
2、BigDecimal
的对象如何获取?
-
如果小数比较大,超过了
double
的取值范围,就可以直接通过构造方法new
出来BigDecimal bd1 = new BigDecimal("较大的小数");
-
但是如果没有超出
double
的范围,就可以通过valueOf(val)
的方式来获取
3、常见操作
在除的时候我们要知道,我们是可以给它设置 舍入模式
的,最为常见的就是四舍五入:RoundingMode.HALF_UP
![image-20240422151447229](https://img-blog.csdnimg.cn/img_convert/20d476083d95c1c0d17b15c133265465.png)