BigDecimal是Java中用于高精度算术运算的类。当您需要精确地处理非常大或非常小的数字时,例如在金融计算中,它特别有用。由于众所周知得原因,Double这种类型在某些情况下会出现丢失精度的问题,所以在需要对较为敏感的数据(比如与金额有关的)进行运算时,我们都会用BigDecimal。但是,用BigDecimal不代表就一定没问题,我们今天就讨论一下关于BigDecimal的问题。

精度与刻度

要正确使用BigDecimal,首先要清楚精度(precision)和刻度(scale)的概念。

  • Precision(精度):表示数值的总位数,包括小数点前后的位数。例如,数值 123.45 的精度是 5,因为它有 5 位数字。

  • Scale(刻度):表示小数点后的位数。例如,数值 123.45 的刻度是 2,因为小数点后有 2 位数字。

举个例子,如果一个数值类型定义为 DECIMAL(7, 2),那么它的精度是 7,刻度是 2。这意味着这个数值最多可以有 7 位数字,其中 2 位在小数点后,5 位在小数点前。

(p.s. DECIMAL这个数值类型通常是用在数据库中的,JAVA中并没有这个类型。用这个例子是因为它可以最清晰地说明精度与刻度)

BigDecimal类中也有获取精度和刻度的方法

    BigDecimal num = new BigDecimal("12.1234");
System.out.println(String.format("precision:%s scale:%s", num.precision(),num.scale())); //输出:precision:6 scale:4

除法中的刻度

在用BigDecimal做除法运算,使用divide方法的时候,可以指定刻度,也可以不指定。

当指定刻度,即保留几位小数的时候,需要指定进位模式(RoundingMode)。

可选的模式有UP、DOWN、CEILING、FLOOR、HALF_UP、HALF_DOWN、HALF_EVEN、UNNECESSARY。

JDK api中用一个表格比较了这几种模式的区别

Result of rounding input to one digit with the given rounding mode

Input Number UP DOWN CEILING FLOOR HALF_UP HALF_DOWN HALF_EVEN UNNECESSARY
5.5 6 5 6 5 6 5 6 throw ArithmeticException
2.5 3 2 3 2 3 2 2 throw ArithmeticException
1.6 2 1 2 1 2 2 2 throw ArithmeticException
1.1 2 1 2 1 1 1 1 throw ArithmeticException
1.0 1 1 1 1 1 1 1 1
-1.0 -1 -1 -1 -1 -1 -1 -1 -1
-1.1 -2 -1 -1 -2 -1 -1 -1 throw ArithmeticException
-1.6 -2 -1 -1 -2 -2 -2 -2 throw ArithmeticException
-2.5 -3 -2 -2 -3 -3 -2 -2 throw ArithmeticException
-5.5 -6 -5 -5 -6 -6 -5 -6 throw ArithmeticException

按指定的规则进位,保留几位小数,这没有问题。

如果不指定刻度呢?

    BigDecimal one = new BigDecimal("1");
BigDecimal eight = new BigDecimal("8");
System.out.println(one.divide(eight));//输出 0.125
BigDecimal three = new BigDecimal("3");
System.out.println(one.divide(three));// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

当结果能除尽的时候正常处理,当除不尽即结果是无限循环小数的时候,程序抛出异常。

看一下源码:

public BigDecimal divide(BigDecimal divisor) {
/*
* Handle zero cases first.
*/
if (divisor.signum() == 0) { // x/0
if (this.signum() == 0) // 0/0
throw new ArithmeticException("Division undefined"); // NaN
throw new ArithmeticException("Division by zero");
} // Calculate preferred scale
int preferredScale = saturateLong((long) this.scale - divisor.scale); if (this.signum() == 0) // 0/y
return zeroValueOf(preferredScale);
else {
/*
* If the quotient this/divisor has a terminating decimal
* expansion, the expansion can have no more than
* (a.precision() + ceil(10*b.precision)/3) digits.
* Therefore, create a MathContext object with this
* precision and do a divide with the UNNECESSARY rounding
* mode.
*/
MathContext mc = new MathContext( (int)Math.min(this.precision() +
(long)Math.ceil(10.0*divisor.precision()/3.0),
Integer.MAX_VALUE),
RoundingMode.UNNECESSARY);
BigDecimal quotient;
try {
quotient = this.divide(divisor, mc);
} catch (ArithmeticException e) {
throw new ArithmeticException("Non-terminating decimal expansion; " +
"no exact representable decimal result.");
} int quotientScale = quotient.scale(); // divide(BigDecimal, mc) tries to adjust the quotient to
// the desired one by removing trailing zeros; since the
// exact divide method does not have an explicit digit
// limit, we can add zeros too.
if (preferredScale > quotientScale)
return quotient.setScale(preferredScale, ROUND_UNNECESSARY); return quotient;
}
}

MathContext mc = new MathContext( (int)Math.min(this.precision() +(long)Math.ceil(10.0*divisor.precision()/3.0),Integer.MAX_VALUE),RoundingMode.UNNECESSARY);

这一句算了一个刻度,然后进位方式选了UNNECESSARY。所以当除不尽的时候就抛异常了。

也就是说它断定,如果一个除法能除尽,它的小数位小于this.precision() +(long)Math.ceil(10.0*divisor.precision()/3.0)

这句话的意思是,如果 this 除以 divisor 的商是一个有限小数,那么这个小数的位数不会超过以下公式计算的结果:

\(a.precision() + \lceil \frac{10 \cdot b.precision}{3} \rceil\)

其中:

• a.precision() 是 this 的精度,即 this 的总位数。

• b.precision() 是 divisor 的精度,即 divisor 的总位数。

• ceil 是向上取整函数。

这个公式的作用是估算有限小数的最大位数。具体来说,它考虑了 this 和 divisor 的精度,并通过一个比例关系来确定商的小数位数上限。

举个例子,如果 this 的精度是 5,divisor 的精度是 3,那么公式计算如下:

\(5 + \lceil \frac{10 \cdot 3}{3} \rceil = 5 + \lceil 10 \rceil = 5 + 10 = 15\)

因此,商的有限小数位数不会超过 15 位。

插一句啊,这是为什么?

AI是这样回答的:

这个公式的提出者并没有明确的记录,但它基于数论中的一些基本原理。证明这个公式需要用到基本算术定理(Fundamental Theorem of Arithmetic),即每个整数都可以唯一地分解为素数的乘积。

证明思路

  1. 有限小数的条件:

    一个分数 \(\frac{a}{b}\)只有在其分母 (b) 仅包含 2 和 5 作为素因子时,才会有一个有限小数。这是因为 10 的唯一素因子是 2 和 5。
  2. 小数位数的估算:

    假设我们有一个分数 \(\frac{a}{b}\),其中 (a) 和 (b) 是整数,并且 (b) 仅包含 2 和 5 作为素因子。我们可以将 (b) 表示为 \((2^m \times 5^n)\),其中 (m) 和 (n) 是非负整数。
  3. 小数位数的上限:

    为了使分数 \(\frac{a}{b}\) 变成一个整数,我们需要将其乘以 \((10^{\max(m, n)})\)。因此,有限小数的最大位数可以通过以下公式估算:

    \(a.precision + \lceil \frac{10 \cdot b.precision}{3} \rceil\)

    其中 (a.precision) 是分子 (a) 的位数,(b.precision) 是分母 (b) 的位数。

具体例子

假设我们有两个数:

• this = 12345(精度为 5,因为有 5 位数字)

• divisor = 678(精度为 3,因为有 3 位数字)

我们想知道 12345 除以 678 的商,如果是有限小数,它的小数部分最多有多少位。根据公式:

\(5 + \lceil \frac{10 \cdot 3}{3} \rceil = 5 + \lceil 10 \rceil = 5 + 10 = 15\)

因此,12345 除以 678 的商,如果是有限小数,小数部分最多有 15 位。

额...是数论啊?那我走,打扰了,打扰了...

为什么你应该用字符串来构造BigDecimal

聪明的你应该早就发现了,BigDecimal的构造方法有很多个。应该用哪个呢?很多人都知道应该用字符串,可是为什么呢?

因为,当你不用字符串的时候,会用很多意想不到的惊喜。

    BigDecimal strnum = new BigDecimal("12.1234");
System.out.println(String.format("precision:%s scale:%s", strnum.precision(),strnum.scale()));//输出 precision:6 scale:4 BigDecimal num = new BigDecimal(12.1234);
System.out.println(String.format("precision:%s scale:%s", num.precision(),num.scale()));//输出precision:50 scale:48

下面一个刻度是48,这是什么鬼?

再试试除法

    BigDecimal one = new BigDecimal("0.1");
BigDecimal eight = new BigDecimal("8");
System.out.println(one.divide(eight));//输出 0.0125 BigDecimal _1 = new BigDecimal(0.1);
BigDecimal _8 = new BigDecimal(8);
System.out.println(_1.divide(_8));//输出 0.0125000000000000006938893903907228377647697925567626953125

为什么会出现这种结果?这个原因众所周知:

二进制(也称为基数2)不能精确表示某些十进制数,尤其是那些在十进制中有有限小数位但在二进制中需要无限小数位的数。例如,0.1 和 0.2 在二进制中无法精确表示。

这是因为二进制系统只能使用 0 和 1 来表示数值,而某些十进制数在转换为二进制时会变成无限循环小数。例如:

• 0.1 在二进制中表示为 0.00011001100110011...(无限循环)

• 0.2 在二进制中表示为 0.0011001100110011...(无限循环)

由于计算机的存储空间有限,这些无限循环小数只能被截断,从而导致精度损失。这就是为什么在使用浮点数进行计算时,可能会出现精度问题。

如果你看源码你会发现 public BigDecimal(double val) 和 public BigDecimal(String val)的实现完全不同。或许你也会想为什么呢?为什么要实现两套不同的呢?你就直接这样:

public BigDecimal(double val) {
this(String.valueOf(val));
}

不就完了?

至于JDK团队实现两套的原因是什么?你知道吗?欢迎留言告诉我:)


著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

原文: https://wangxuan.me/tech/2024/07/16/the-precision-and-scale-of-BigDecimal.html

BigDecimal的精度与刻度的更多相关文章

  1. BigDecimal带精度的运算的两篇文章

    转自:http://guoliangqi.iteye.com/blog/670908 之前提到过在商业运算中要使用BigDecimal来进行相关的钱的运算(java中关于浮点运算需要注意的 ),可是实 ...

  2. 简单BigDecimal运算精度

    项目中遇到了数值运算,如网上所写的,一般有这几个方法: /** * 提供精确的加法运算. * @param v1 被加数 * @param v2 加数 * @return 两个参数的和 */ publ ...

  3. Java中的BigDecimal类精度问题

    bigdecimal 能保证精度的原理是:BigDecimal的解决方案就是,不使用二进制,而是使用十进制(BigInteger)+小数点位置(scale)来表示小数,就是把所有的小数变成整数,记录小 ...

  4. BigDecimal类(精度计算类)的加减乘除

    BigDecimal类 对于不需要任何准确计算精度的数字可以直接使用float或double,但是如果需要精确计算的结果,则必须使用BigDecimal类,而且使用BigDecimal类也可以进行大数 ...

  5. hibernate中设置BigDeCimal的精度

    @Column(precision = 12, scale = 2) 在MySQL数据库中的精度为:

  6. 在进行商业运算时解决BigDecimal的精度丢失问题

    System.out.println(0.05+0.01); System.out.println(1.0-0.42); System.out.println(4.015*100); System.o ...

  7. java中四舍五入——double转BigDecimal的精度损失问题

    代码: double d = -123456789012345.3426;//5898895455898954895989; NumberFormat nf = new DecimalFormat(& ...

  8. BigDecimal的精度舍入模式详解

    BigDecimal舍入模式介绍: 舍入模式在java.math.RoundingMode 里面: RoundingMode.CEILING :向正无限大方向舍入的舍入模式.如果结果为正,则舍入行为类 ...

  9. Java中关于 BigDecimal 的一个导致double精度损失的"bug"

    背景 在博客 恶心的0.5四舍五入问题 一文中看到一个关于 0.5 不能正确的四舍五入的问题.主要说的是 double 转换到 BigDecimal 后,进行四舍五入得不到正确的结果: public ...

  10. BigDecimal 小数 浮点数 精度 财务计算

    简介 float和double类型的使用局限: 单精度浮点型变量float可以处理6~7位有效数,双精度浮点型变量double可以处理15~16位有效数,在实际应用中,如果需要对更大或者更小的数进行运 ...

随机推荐

  1. ansible使用详解

    ansible执行,用户主机配置 免密同一个同一个用户执行命令 1 能免密登录的[root@mcw1 ~]$ ansible 10.0.0.132 -m shell -a "hostname ...

  2. Linux/Golang/glibC系统调用

    Linux/Golang/glibC系统调用 本文主要通过分析Linux环境下Golang的系统调用,以此阐明整个流程 有时候涉略过多,反而遭到质疑~,写点文章证明自己实力也好 Golang系统调用 ...

  3. 日常Bug排查-偶发性读数据不一致

    日常Bug排查-偶发性读数据不一致 前言 日常Bug排查系列都是一些简单Bug的排查.笔者将在这里介绍一些排查Bug的简单技巧,同时顺便积累素材. Bug现场 业务场景 先描述这个问题出现的业务场景. ...

  4. 聊聊 JSON Web Token (JWT) 和 jwcrypto 的使用

    哈喽大家好,我是咸鱼. 最近写的一个 Python 项目用到了 jwcrypto 这个库,这个库是专门用来处理 JWT 的,JWT 全称是 JSON Web Token ,JSON 格式的 Token ...

  5. git cherry-pick合并其它分支的某次提交(commits)到当前分支

    git cherry-pick 可以选择某一个分支中的一个或几个commit(s)来进行操作. 例如,假设我们有个稳定版本的分支,叫v2.0.0,另外还有个开发版本的分支v3.0.0,我们不能直接把两 ...

  6. 基于webapi的websocket聊天室(番外二)

    我比较好奇的是webapi服务器怎么处理http请求和websocket请求.有了上一篇番外的研究,这里就可以试着自己写个非常简易的webapi服务器来接收这两种请求. 效果 http请求 消息打印 ...

  7. Android 13 - Media框架(20)- ACodec(二)

    关注公众号免费阅读全文,进入音视频开发技术分享群! 这一节开始我们就来学习 ACodec 的实现 1.创建 ACodec ACodec 是在 MediaCodec 中创建的,这里先贴出创建部分的代码: ...

  8. aardio桌面软件开发 简单,打包后文件小,支持 .net python 和 众多插件

    aardio 编程语言 - 官网 aardio  专注于桌面软件开发,17年一直保持非常活跃地更新( 更新日志 ),aardio 被多年用于生产项目实践,久经测试和锤炼.aardio 在诞生之初就设计 ...

  9. ETL工具-nifi干货系列 第三讲 nifi web ui 使用教程

    1.nifi 服务启动之后,浏览器输入https://localhost:8443/nifi ,匿名登录或者输入用户名密码进入操作页面,如下图所示: 2.组件工具栏 处理器,鼠标放到图标上提示Proc ...

  10. P7897

    problem && blog 第一道正经的 Ynoi,特此写篇题解纪念一下. Algorithm 1 可以想到 \(O(nm)\) 的 DP. 我们定义 \(dp_u\) 为 \(u ...