首页 > 其他分享 >BigDecimal的精度与刻度

BigDecimal的精度与刻度

时间:2024-07-17 14:52:10浏览次数:14  
标签:scale BigDecimal ArithmeticException precision 刻度 new 精度 divisor


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

标签:scale,BigDecimal,ArithmeticException,precision,刻度,new,精度,divisor
From: https://www.cnblogs.com/wang1dao/p/18307346

相关文章

  • java导入excel数据,要求数据精度与文件一致
    最近应客户需求,导入excel表格,且要求数据精度和日期格式与文件一致。之前虽然做过导入导出的功能,但要求没有这么细致,因此在网上查找了大量的文件,找到了表格的cell.getCellStyle().getDataFormatString()这个属性,可以根据属性在程序里转换成自己需要的格式。publicStringgetC......
  • 机器学习分类结果精度测定 - 混淆矩阵(Confusion Matrix)
    一、引言机器学习和数据科学中一个经常被忽视,但至关重要的概念是模型评估。你可能已经建立了一个非常先进的模型,但如果没有合适的评估机制,你就无法了解模型的效能和局限性。这就是混淆矩阵(ConfusionMatrix)派上用场的地方。1.1什么是混淆矩阵?混淆矩阵是一种特定的表格布局......
  • (二十三)高精度运算
    文章目录一、前言二、正文2.1什么是高精度?2.2高精度加2.2.1理论2.2.2怎么敲代码?2.2.2.1存储两个数2.2.2.2倒序存入数组2.2.2.3开始计算2.2.2.4结果更改2.2.2.5最终代码2.3高精度减2.3.1理论2.3.2怎么敲代码?2.3.2.1存储2.3.2.2倒序存入数组2.3.2.3计算2......
  • 科普 | 3分钟读懂高精度时间同步
    通过上一期的内容我们知道北京时间作为我们国家的标准时间,是由中国科学院国家授时中心产生、保持和发播的。中国科学院国家授时中心位于陕西西安临潼,那北京时间诞生于这里,是怎么做到让全国人民都用上北京时间呢,大家又是通过什么手段和北京时间实现同步的呢?看完本期内容,相信你......
  • DW03D是一款用于锂离子/聚合物电池保护的高集成度解决方案。DW03D包含内部功率MOSFET
    一般概述    DW03D产品是单节锂离子/锂聚合物可充电电池组保护的高集成度解决方案。DW03D包括了先进的功率MOSFET,高精度的电压检测电路和延时电路。    DW03D具有非常小的TSS08-8的封装,这使得该器件非常适合应用于空间限制得非常小的可充电电池组应用。 ......
  • 高精度
    高精度高精度,即无法使用C++本身配置的数据类型中使用的运算高精度加法例题:P1601A+BProblem主要方法:使用字符串存储数字转换为整型数组模拟竖式加法逆序输出Code#include<iostream>usingnamespacestd;#defineMAXN10005strings1,s2;inta[MAXN],b[MAXN],c......
  • c++ u7-02-高精度乘法
    本节课作业:链接:https://pan.baidu.com/s/13-FC86jSHGziRDA8lqzimg?pwd=owv1提取码:owv1   高精度乘法             #include<iostream>#include<cstdio>#include<cstring>usingnamespacestd;stringx,y;inta......
  • SLAM 精度评估
    SLAM精度的评估有两个最重要的指标,即绝对轨迹误差(ATE)和相对位姿误差(RPE)的均方根误差(RMSE):绝对轨迹误差:直接计算相机位姿的真实值与SLAM系统的估计值之间的差值,首先将真实值与估计值的时间戳对齐,然后计算每对位姿之间的差值,适合于评估SLAM系统的性能。相......
  • 高精度模板
    转载自wkh2008。#include<bits/stdc++.h>usingnamespacestd;namespaceBIGINT{usingcpx=complex<double>;constdoublePI=acos(-1);vector<cpx>roots={{0,0},{1,0}};voidensure_capacity(intmin_capacity){for(intlen=r......
  • 28-BigDecimal类
    BigDecimal精确存储,精度要求非常高时候用这类这个类的作用就是精确计算浮点数doubled1=1.0;doubled2=0.9;System.out.println(d1-d2);//0.09999999999999998doubleresult=(1.4-0.5)/0.9;System.out.println(result);......