在 BigDecimal.divide 期间引发 ArithmeticException

Posted

技术标签:

【中文标题】在 BigDecimal.divide 期间引发 ArithmeticException【英文标题】:ArithmeticException thrown during BigDecimal.divide 【发布时间】:2011-02-14 12:23:40 【问题描述】:

我认为java.math.BigDecimal 应该是对十进制数执行无限精度算术需求的 The Answer™。

考虑以下 sn-p:

import java.math.BigDecimal;
//...

final BigDecimal one = BigDecimal.ONE;
final BigDecimal three = BigDecimal.valueOf(3);
final BigDecimal third = one.divide(three);

assert third.multiply(three).equals(one); // this should pass, right?

我预计 assert 会通过,但实际上执行甚至没有到达那里:one.divide(three) 导致 ArithmeticException 被抛出!

Exception in thread "main" java.lang.ArithmeticException:
Non-terminating decimal expansion; no exact representable decimal result.
    at java.math.BigDecimal.divide

事实证明,API 中明确记录了这种行为:

divide 的情况下,精确商可以有无限长的十进制扩展;例如,1 除以 3。如果商具有非终止十进制扩展并且指定运算返回精确结果,则抛出 ArithmeticException。否则,将返回除法的确切结果,就像其他操作一样。

进一步浏览API,发现实际上divide有各种重载执行不精确除法,即:

final BigDecimal third = one.divide(three, 33, RoundingMode.DOWN);
System.out.println(three.multiply(third));
// prints "0.999999999999999999999999999999999"

当然,现在明显的问题是“有什么意义???”。我认为BigDecimal 是我们需要 exact 算术时的解决方案,例如用于财务计算。如果我们甚至不能准确地divide,那么这有多大用处?它实际上是用于通用目的,还是仅在非常小众的应用程序中有用,幸运的是,您根本不需要divide

如果这不是正确的答案,我们在财务计算中使用什么可以进行精确除法? (我的意思是,我没有金融专业,但他们仍然使用除法,对吗???)。

【问题讨论】:

顺便说一句,我非常感谢那些使用过财务软件的人的见解。我经常听到BigDecimaldouble 提倡,我想听听他们如何处理divide 我正在开发一个非常复杂的 Java 应用程序,在 JTables 中显示大量财务信息(顺便说一句,JTables 如此复杂且经常更新,以至于我不得不使用几个 java.sun.com/products/jfc/tsc/articles/ChristmasTree 优化来获得我的 JTables 在快速机器上的表现可以接受)。 突出的两件事:单位通常是美分,我有自己的 NumDiv 理性抽象。请注意,这不是 Java 平台的缺点:最坏的情况是默认 JAVA API 的缺点(如果有的话)。 【参考方案1】:

如果这不是正确答案,我们可以使用什么来进行财务计算中的精确除法? (我的意思是,我没有金融专业,但他们仍然使用除法,对吗???)。

那时我还在上小学1,他们告诉我,当你除以 1 除以 3 时,你会得到 0.33333... 即一个循环小数。以十进制形式表示的数字除法并不准确。事实上,对于任何固定的基数,都会有分数(一个整数除以另一个整数的结果)不能准确地表示为该基数中的有限精度浮点数。 (这个数字会有一个重复的部分......)

当您进行涉及除法的财务计算时,您必须考虑如何处理循环分数。您可以将其向上或向下舍入,或舍入到最接近的整数或其他值,但基本上您不能只是忘记这个问题。

BigDecimal javadoc 是这样说的:

BigDecimal 类让用户可以完全控制舍入行为。如果没有指定取整方式,无法表示准确的结果,则抛出异常;否则,可以通过向操作提供适当的 MathContext 对象来执行所选精度和舍入模式的计算。

换言之,您有责任告诉 BigDecimal 如何进行舍入。

EDIT - 回应 OP 的这些后续行动。

BigDecimal 如何检测无限循环小数?

它没有明确检测循环小数。它只是检测到某些操作的结果不能用指定的精度精确表示;例如为了精确表示,小数点后需要太多数字。

它必须跟踪并检测股息中的一个循环。它可以选择以另一种方式处理这个问题,通过标记重复部分的位置等。

我想BigDecimal 可以被指定为精确地表示循环小数;即作为BigRational 类。但是,这会使实现更复杂,使用成本更高2。而且由于大多数人都希望数字以十进制显示,并且在这一点上重复出现小数的问题。

最重要的是,这种额外的复杂性和运行时成本对于BigDecimal 的典型用例是不合适的。这包括财务计算,其中会计惯例不允许您使用循环小数。


1 - 这是一所优秀的小学...

2 - 您要么尝试删除除数和被除数的公因子(计算成本高),要么让它们无限制地增长(空间使用成本高......并且计算上用于以后的操作)。

【讨论】:

BigDecimal 如何检测无限循环小数?它必须跟踪并检测股息中的周期。它可以选择以另一种方式处理这个问题,通过标记重复部分的位置等。 @Stephen:非常感谢您对它如何检测无限扩展的调查。发表评论后我也去了,但我想我应该给你一个机会自己回答=)【参考方案2】:

班级是BigDecimal 而不是BigFractional。从你的一些 cmets 听起来你只是想抱怨有人没有在这个类中构建所有可能的数字处理算法。金融应用不需要无限的小数精度;只是完全精确到所需精度的值(通常为 0、2、4 或 5 个十进制数字)。

其实我处理过很多使用double的金融应用。我不喜欢它,但那是它们的编写方式(也不是用 Java 编写的)。当存在汇率和单位换算时,就会存在四舍五入和瘀伤问题的可能性。 BigDecimal 消除了后者,但仍有前者用于除法。

【讨论】:

+1。你用“BigDecimal 而不是BigFractional”搞定了。感谢您对 double 金融应用的见解。【参考方案3】:

如果您想使用小数,而不是有理数,并且在最终舍入之前需要精确的算术运算(舍入到美分或其他值),这里有一个小技巧。

您始终可以操纵您的公式,以便只有一个最终除法。这样您就不会在计算过程中失去精度,并且您将始终获得正确的舍入结果。比如

a/b + c

等于

(a + bc) / b.

【讨论】:

哦,所有工程师都在做什么,在这个时代,每当我们不得不手动操作计算机时,我们都应该畏缩不前。 【参考方案4】:

顺便说一句,我真的很感激 来自与之合作过的人的见解 财务软件。我经常听到 BigDecimal 被提倡超过 double

在财务报告中,我们总是使用比例 = 2 和 ROUND_HALF_UP 的 BigDecimal,因为报告中的所有打印值都必须导致可重现的结果。如果有人用一个简单的计算器检查这个。

在瑞士,他们四舍五入到 0.05,因为他们不再有 1 或 2 个Rappen 硬币。

【讨论】:

【参考方案5】:

您应该更喜欢 BigDecimal 进行财务计算。四舍五入应由业务指定。例如。一笔金额(100,00 美元)必须平均分配给三个账户。必须有一个业务规则,该帐户需要额外的一分钱。

Double,浮点数不适合在金融应用中使用,因为它们不能精确地表示 1 的分数,而不是 2 的指数。例如考虑 0.6 = 6/10 = 1*1/2 + 0*1/4 + 0*1/8 + 1*1/16 + ... = 0.1001...b

对于数学计算,您可以使用符号数,例如存储分母和分子甚至整个表达式(例如,这个数字是 sqrt(5)+3/4)。因为这不是 java api 的主要用例,所以你不会在那里找到它。

【讨论】:

【参考方案6】:

有没有必要

a=1/3;
b=a*3;

resulting in

b==1;

在金融系统中?我猜不会。在金融系统中,它定义了在进行计算时必须使用哪种循环模式和比例。在某些情况下,roundmode 和 scale 是在法律中定义的。所有组件都可以依赖这种定义的行为。返回 b==1 将是失败的,因为它不会满足指定的行为。这在计算价格等时非常重要。

这就像 IEEE 754 规范以二进制数字表示浮点数。组件不能在不丢失信息的情况下优化“更好”的表示,因为这会破坏合同。

【讨论】:

double 是否满足金融部门的合同? double 也不能控制舍入模式等吗? 处理必须平衡的数量时,执行除法的正确方法是同时保留商和余数。将 1.00 美元分给 7 个人会发现每个人都有 0.14 美元,外加两美分,可以根据某些政策分配。【参考方案7】:

要分割保存,必须设置MATHcontext

BigDecimal bd = new BigDecimal(12.12, MathContext.DECIMAL32).divide(new BigDecimal(2)).setScale(2, RoundingMode.HALF_UP);

【讨论】:

【参考方案8】:

我承认 Java 对表示分数没有很好的支持,但您必须意识到在使用计算机时不可能保持事物完全精确。至少在这种情况下,异常告诉您精度正在丢失。

据我所知,“十进制数的无限精度算术”是不会发生的。如果您必须使用小数,那么您所做的可能很好,只需捕获异常即可。否则,快速 google 搜索会发现一些在 Java 中使用分数的有趣资源:

http://commons.apache.org/math/userguide/fraction.html

http://www.merriampark.com/fractions.htm

Best way to represent a fraction in Java?

【讨论】:

完全精确是可能的。有一种东西叫做分数。只是我们现有的库是有限的。 @Pacerier - 不是真的;考虑无理数。 对于无理数,库会以表达式的形式存储数字。例如。 sqrt 2 存储为 sqrt 2,而不是 1.4142135 ......等等。这正是我们在高中数学中的做法;如果预解析会给我们带来不精确的结果,那么我们只在需要显示时才解析最后一步中的数字。计算机没有理由不能做到这一点。 @Pacerier 给予足够的时间、cpu 和内存。它的效率太低了(尽管您可能会为此部分归咎于计算机体系结构)。喜欢就试试吧;在那之前我不会责怪图书馆。 BigInteger 与原始 int 相比也“效率太低”。这就是代码库的全部意义:编写“效率太低”的代码,当用例可能牺牲功能的速度时,人们会使用这些代码。 “效率太低”并不是反对构建专用 BigRealNumber 库的理由。【参考方案9】:

请注意,我们使用的是计算机...计算机有很多内存,而精度需要内存。因此,当您想要无限精度时,您需要 (infinite * infinite) ^ (infinite * Integer.MAX_VALUE) 太字节内存...

我知道1 / 30.333333...,应该可以像“一除以三”一样将它存储在内存中,然后你可以将它乘回去,你应该有1。但我不认为 Java 有这样的东西... 也许你必须因为写出这样的东西而赢得诺贝尔奖。 ;-)

【讨论】:

“Maple 支持硬件(双)精度和无限精度计算。” maplesoft.com/products/maple/compare/numeric_computation.aspx 我不会对像 Maple 这样的系统有任何期望! :) 看看更“通用”的语言(如 Java),Python 确实内置了对有理数的支持:docs.python.org/library/fractions.html @Martin:是的,RAM 是有限的,但仍有一些方法可以象征性地表示无限。看看double:它有一个POSITIVE_INFINITYNEGATIVE_INFINITYdouble 只有 64 位。计算机本可以存储.(3),其中(number) 是重复部分。事实上,很多实现已经这样做了(不,他们并没有因此获得诺贝尔奖)。 数学、计算机科学或任何工程学科没有诺贝尔奖。对不起:-)

以上是关于在 BigDecimal.divide 期间引发 ArithmeticException的主要内容,如果未能解决你的问题,请参考以下文章

Java里面BigDecimal.divide()方法能不能实现保留小数点前后共35位这样的操作

Java里面BigDecimal.divide()方法能不能实现保留小数点前后共35位这样的操作

BigDecimal类整除报错的解决方案

java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal res

在引发 ValidationError 期间“未定义全局名称'_'”

AdaptiveCards 在 .NET 解析期间引发异常