【Java】BigDecimal类

一、引入

我们先来看一段代码,程序结果如下

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

为什么是这样的呢?

我们可以把二进制再转回十进制来看一看。

小数部分转十进制的方式和整数部分是一样的,都是乘以 基数的权次幂, 然后再相加。

要注意的是:整数部分的权是 0、1、2、3、4... 这样依次递增的;而小数部分这里的权是 -1、-2、-3 这样依次递减的。

image-20240422124350496

因此知道了,我们刚刚转为的二进制数是没有任何问题的。

但是由于现在我们研究的是小数部分的不精确性,因此现在只看小数部分的二进制。


0.875 是一个比较特殊的数字,小数部分用 111 就能表示了。

image-20240422124718926

但假如此时我换一个数字:0.9,此时它小数部分的二进制就非常非常长了,需要使用 45位 才能够表示。

image-20240422124806763

如果再换一个数字 0.226,它小数部分的二进制就更长了,需要用 55位 才能表示。

image-20240422124923838

由此可见,如果我们把十进制的小数转成二进制,那么它的结果有可能会很长很长很长很长

而在Java中,floatdouble 在底层占用的字节数是有限的。

如果超出了小数部分bit位数,就会进行舍弃。

image-20240422125055171

我们用这个理论去看上面的 0.226,它的小数部分一共有 55位,如果我用 double 去记录它,前面的 52 位我可以留着,但是后面还有三位,就只能舍弃了。

因此小数在存储的时候,有可能就是不精确的。

当我用一个不精确的数据去参与计算,不管是 加减乘除,结果都有可能是不精确的。

但是在实际开发中如果你的数据不精确,大多数情况下可能问题不大。

但是有些特殊的场景它的问题就很大了,它是需要我们进行精确运算的。

例如在银行里就是这样的:假设以后我们买房子,贷款 100万,按照 4.9% 的利息还 30年,计算出每个月要还的钱,结果就是右边的图。

这个时候就需要精确运算了,而且小数点需要保留后面的两位。

image-20240422125605039

除此之外,在飞机、火箭这些精密零件上,也必须要有精确运算。

所以说在实际开发中,像一些金融、证券、精密零件计算… 都需要用到小数点精确运算。

为了解决这个问题,Java就提供了一个类:BigDecimal,这个类就可以帮助我们进行小数的精确运算。


三、BigDecimal 的作用

  • 用于小数的精确计算

  • 用来表示很大的小数

    这个特点跟 BigInteger 是一样的

那么 BigDecimal 这个类该如何用呢?我们可以到 API帮助文档 中查看一下。


四、阅读 API帮助文档

查看API文档,我们可以看到API文档中关于BigDecimal类的定义如下:

image-20240422130149962

首先看它的包,它是在 java.math包 下的。

image-20240422130220683

再往下,就是它的继承结构:它的父类是 Number,爷爷是 Object

image-20240422130240453

再往下,去看它的一些描述:BigDecimal 是不可变的、任意精度的有符号十进制数。

不可变 ,这个特点跟 BigInteger 的特点是一样的,一旦创建对象之后,不管是做 加减乘除 等操作,它原本的值就不会再变了,而是显示一个新的 BigDecimal对象;并且 任意精度 可知,精度 也可以得到保证。

再往下,我们就要去看它的构造方法。

可以发现它有很多很多的构造,你可以根据不同的情况去选择不同的构造,在这里我会跟大家介绍几个构造。

下面这个构造是将一个小数变成 BigDecimal 对象,我们用鼠标点击一下这个蓝色的。

image-20240422130805179

就可以看到这个构造方法的介绍。

double 转换为 BigDecimal,后者是 double 的二进制浮点值准确的十进制表示形式。

但是在这,它会有一个小小的注意点:此构造方法的结果有一定的不可预知性。

image-20240422131339775

也就是说如果你用这种方式去创建一个 BigDecimal 对象的话,它的结果还有可能是不精确的。

那怎么办?我需要精确运算。


如果你想要精确运算,那就需要用到下面这个构造:跟 BigInteger 一样,它还是通过一个字符串的表示形式将它变成 BigDecimal 的对象,我们用鼠标点击一下它,它就会跳转到详情界面。

image-20240422131044238

往下翻,这里它也有一个注意点:它不会遇到 BigDecimal(double) 构造方法的不可预知问题。

image-20240422131226483

也就是说我们用这种方式去计算的话,它的结果就是精确的。


五、构造方法代码示例

1)BigDecimal(double)

首先,通过传递double类型的小数来创建对象

BigDecimal bd1 = new BigDecimal(0.01);
BigDecimal bd2 = new BigDecimal(0.09);

bd1bd2 分别来做一个打印

System.out.println(bd1);
System.out.println(bd2);

程序运行结果如下,都不用我们去进行 加减乘除 运算,结果已经就不精确了。

image-20240422131533560

因此,这种方式有可能是不精确的,所以不建议使用。因此接下来说第二个,建议大家使用的。


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

并且我们可以来运算一下,看产生的新的 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

我们反过来来看看 一、引入 中写的案例:小数直接运算,它的结果就是不精确的。

image-20240422132127844

但是我现在使用 BigDecimal 运算,它的结果就是精确的,这个就是 BigDecimal 最大的特点。


3)通过静态方法 valueOf(double val) 获取对象

BigDecimalBigInteger 一样,在 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)查看源码

选中 BigDecimalctrl + b 跟进,ctrl + F12 搜一下 valueOf,我们先带着大家去看参数是 double 类型的这个方法。

image-20240422133401770

这个方法的原码非常的简单,它是直接把传递过来的数字,变成字符串,然后再传递给构造方法创建对象就行了。

因此它底层其实也是 new 出来的。

image-20240422133506140

另一个 valueOf 方法我们也来看一下,这次我们要找的是参数是 long类型 的。

image-20240422133624157

这个方法的底层稍微来讲就复杂很多。

image-20240422133703552

下面我们来逐行分析一下

// 首先拿着参数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

回到上一步,继续分析 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,表示 b6b7 其实是同一个对象。

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

image-20240422134851874


六、常见的成员方法

它的成员方法跟 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

JDK9的时候这种使用 BigDecimal中的常量 的写法已经过时了

image-20240422140518587

Java 觉得这些东西是一些计算的保留方式,如果直接将它放到 BigDecimal 中感觉不太好,因此Java就将这些 计算的摄入模式,单独的写到一个类中,这个类叫:RoundingMode

image-20240422140837539

在这个枚举类中定义了很多种取舍方式。

image-20240422140911763

这里用 四舍五入 举例,程序运行完毕,可以发现还是 3.33

BigDecimal bd6 = bd1.divide(bd2, 2, RoundingMode.HALF_UP);
System.out.println(bd6); // 3.33

八、舍入模式

在Java中它其实给我们规定了很多舍入模式,打开一下文档来看一下

找到成员方法 divide() ,参数为 RoundingMode 的重载方法,用鼠标点击一下这里的 RoundingMode

image-20240422141118337

RoundingMode 是一个枚举,枚举我们还没有学习,现在我们可以把它也理解成是一个类,它里面用到的这些东西,就可以把它理解为:用 public static final 所修饰的常量,后面会有一个单独的章节去讲解这个枚举的。

此时就可以看见,在Java中给我们规定了这么多的舍入模式

image-20240422141331585

往下拉,它对每一种舍入模式都做了一个详解,这个解释光用嘴巴说说不清楚,我们需要通过数轴的方式来理解。


1)UP

左右一边画图一边对比API文档。

UP:远离零方向舍入的舍入模式。

什么叫做 远离零

针对于正数来讲,向右是远离零。针对负数来讲,向左是远离零,这个就是 UP

image-20240422141548579

在下面它会有一些举例:如果你是用 5.5 采用 UP 这种舍入模式的话,远离零 那不就是 6 吗。

image-20240422141830401

我们来看个负数:-1.1 采用 UP 这种舍入模式的话,远离零 那不就是 -2 吗。

image-20240422141933994


2)DOWN

DOWN:向零方向舍入的舍入模式。

可以发现跟上面的 UP 刚好是反过来的。

针对整数来说,向零方向 应该是向左。

针对于负数来说,向零方向 应该是向右。

image-20240422142333075

看右边的举例:如果你是用 5.5 采用 DOWN 这种舍入模式的话,向零方向 那不就是 5 吗。

image-20240422142441766

我们来看个负数:-1.6 采用 DOWN 这种舍入模式的话,向零方向 那不就是 -1 吗。

image-20240422142532095


3)CEILING

CEILING:向正无限大方向舍入的舍入模式。

正无限大 跟我们以前在数学中说的 正无穷大 是一样的。

向正无限大方向 这个就很简单了,不管你是正数,还是负数,都是向右的方向。

image-20240422142839610

看右边的举例:如果你是用 5.5 采用 CEILING 这种舍入模式的话,向正无限大方向 那不就是 6 吗。

image-20240422142907117

再来看一个负数,如果你是用 -2.5 采用 CEILING 这种舍入模式的话,向正无限大方向 那不就是 -2 吗。

image-20240422142943485

4)FLOOR

FLOOR:向负无限大方向舍入的舍入模式。

FLOOR 的结果跟 CEILING 就是反过来的。

向负无限大方向 不管你是正数,还是负数,都是向左的方向。

image-20240422143104252

看右边的举例:如果你是用 5.5 采用 FLOOR 这种舍入模式的话,向负无限大方向 那不就是 5 吗。

image-20240422143150650

再来看一个负数,如果你是用 -5.5 采用 CEILING 这种舍入模式的话,向负无限大方向 那不就是 -6 吗。

image-20240422143309707

5)HALE_UP

这个就是我们用到最多的 四舍五入

HALF_UP:此舍入模式就是通常学校里讲的四舍五入。

在前面也有它的说明:向最接近数字方向舍入的舍入模式,如果与两个相邻数字的距离相等(例如 0.512 距离都相等),则向上舍入(进一)。如果被舍弃部分 >= 0.5,则舍入行为同 RoundingMode.UP(进一);否则舍入行为同 RoundingMode.DOWN(舍去)。

image-20240422143806076


6)HALF_DOWN

HALE_UP 四舍五入所类似的还有一个:HALF_DOWN,这两种方式唯一的区别就是:在数字等于 0.5 的时候是不一样的。

HALF_DOWN:如果与两个相邻数字的距离相等,则向下舍入。

image-20240422143816216


这些舍入模式不需要大家去背,只要大家记住 四舍五入:HALE_UP 即可。

但如果我们在实际开发中用到了其他的舍入模式,学会找就行了。

先找 BigDecimal类 ——> 找到除法 divide(),这个方法里面需要我们指定舍入模式 ——> 点击 RoundingMode,然后找你需要的舍入模式就行了。

但一般来说,没有什么特殊的需求,一般我们用的都是 HALE_UP


九、扩展

1)引出结论

我们要来看一下 BigDecimal 在计算机中到底是怎么存储的,它跟 BigInteger 还不太一样。

在这里我还是以 0.226 为例,它的小数部分变成二进制后有 55位

image-20240422145051319

如果我们是按照之前的 BigInteger 的方式,每三十二位去分成一组进行计算的话,这样其实是有一些小弊端的。

0.226 的小数位置其实还不够长,如果我换一个小数,它的二进制更长,有几百位,几千位怎么办?那这种分段进行存储的方式效率就非常的低,因此在Java中,BigDecimal 采取了另外一种存储方式。

我们在获取 BigDecimal 对象的时候,不管你是用 valueOf() 获取,还是 new 通过构造方法获取,其实最终它都是 new 出来的,参数都是一个字符串形式的小数。

image-20240422145318361

Java拿到这个字符串后,它会做这样的事情:遍历,然后得到里面的每一个字符,然后再把这些字符转换成 ASCII码表 中对应的数值再来进行存储。

image-20240422145513379

所以说 BigDecimal 在底层其实也是一个数组,数组里存的是 每一个字符ASCII码表 中所对应的数字。

倘若我现在把数字变一变,变成:123.226,它就是以一个长度为 7 的数组进行存储的。

image-20240422145601432

那如果我现在存的是一个负数,负数在前面还有一个负号,此时在数组当中,它就会把负号所对应的 ASCII码表值 存过来。

PS:如果你存的是正数,它是不会将负号存进去的。

image-20240422145726902


2)验证

我们来到IDEA中采用 Debug模式 验证一下。

将代码赋值到测试类中,右键 Debug

BigDecimal bd1 = BigDecimal.valueOf(0.226);
BigDecimal bd2 = BigDecimal.valueOf(123.226);
BigDecimal bd3 = BigDecimal.valueOf(-1.5);

一直点击下一步,直接让断点走完这三行

image-20240422150040007

先来看 bd1 里面,可以看见它是一个 byte类型 的数组,长度是 5,数组中的数据为:[48, 46, 50, 50, 54]

跟我们刚刚推断的完全一模一样。

image-20240422150413448

再来看 bd2,它里面也是一个 byte 类型的数组,长度是 7,里面的内容是 [49, 50, 51, 46, 50, 50, 54]

跟我们刚刚推断的也是一模一样的。

image-20240422150519131

再来看 bd3,它里面存储的也是一个 byte 类型的数组,长度是 4,里面的内容是 [45, 49, 46, 53]

其中 45 表示的是负号,49146.535

image-20240422150714273

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
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java BigDecimal是用于处理高精度的十进制数值的。它提供了各种数学运算方法,例如加法、减法、乘法和除法,可以对BigDecimal对象进行这些运算操作。 例如,使用add()方法可以对两个BigDecimal对象进行相加操作;使用subtract()方法可以对两个BigDecimal对象进行相减操作;使用multiply()方法可以对两个BigDecimal对象进行相乘操作;使用divide()方法可以对两个BigDecimal对象进行相除操作。 此外,BigDecimal还提供了截断和四舍五入的功能。可以使用setScale()方法来设置保留的小数位数,并且可以指定采用的四舍五入模式。例如,通过设置setScale(4, RoundingMode.HALF_UP)来保留BigDecimal对象的小数位数为4,并且按照四舍五入的方式进行截断。 在使用BigDecimal时,通常会使用其构造方法来创建BigDecimal对象。其中一种常见的构造方法是将double型的数值转换为字符串,然后传递给构造方法,例如new BigDecimal("0.01")。 综上所述,Java BigDecimal可以用于进行高精度的十进制数值计算,并且可以实现截断和四舍五入的功能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [java中的BigDecimal型](https://blog.csdn.net/a1782519342/article/details/124727558)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值