在BigDecimal.divide过程中抛出ArithmeticException异常。

33

我原本认为java.math.BigDecimal应该是用于使用十进制数进行无限精度算术的最佳选择。

请考虑以下代码片段:

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是解决方案。如果我们甚至无法进行除法运算,那么这有多有用呢?它实际上提供了一般用途,还是只在非常小众的应用中有用,在那里你幸运地根本不需要除法
如果这不是正确的答案,那么在金融计算中我们可以使用什么进行精确的除法?(我的意思是,我不是金融专业,但他们仍然使用除法,对吧?)

1
顺便说一下,我非常希望得到那些曾经使用金融软件的人的见解。我经常听到人们支持使用 BigDecimal 而不是 double,我想知道他们如何处理 divide - polygenelubricants
1
我正在开发一个非常复杂的Java应用程序,其中显示了大量的财务信息在JTables中(顺便说一下,JTables非常复杂,而且经常更新,以至于我不得不使用http://java.sun.com/products/jfc/tsc/articles/ChristmasTree/中的几个优化方法来使我的JTables在快速机器上表现得可接受)。两件突出的事情:单位通常是分,我有自己的NumDiv有理数抽象。请注意,这不是Java平台的缺点:最坏的情况下,它是默认JAVA API的缺陷。 - SyntaxT3rr0r
9个回答

25
如果这不是正确的答案,那么在财务计算中我们可以使用什么进行精确除法?(我的意思是,我不是金融专业,但他们仍然使用除法,对吧?)当你用十进制表示法表示数字时,使用除法得到的结果并非精确的。事实上,对于任何固定的“基数”,都会有一些分数(将一个整数除以另一个整数得到的结果),无法以该基数中的有限精度浮点数的形式完全准确地表示。(该数将具有循环部分......)。进行涉及除法的财务计算时,您必须考虑如何处理循环小数。您可以将其四舍五入、向下取整或取最接近的整数或其他方法,但基本上您不能忽略此问题。
BigDecimal javadoc说:
The BigDecimal类使其用户完全控制舍入行为。如果未指定舍入模式且无法表示精确结果,则会抛出异常;否则,通过向操作提供适当的MathContext对象,可以按所选精度和舍入模式进行计算。
换句话说,您需要告诉 BigDecimal如何进行舍入。
编辑 - 对于OP的这些后续回复作出回应。
BigDecimal如何检测无限循环小数?
它不会明确检测循环小数。它只是检测到某些操作的结果不能在指定的精度下完全表示;例如,需要过多的小数点后面的数字以获得准确的表示。
它必须跟踪并检测被除数中的循环,并标记重复部分等方式来处理这个问题。

我认为BigDecimal可以被指定为精确表示循环小数的类,即作为一个BigRational类。但是这会使实现更加复杂并且使用成本更高2。由于大多数人期望数字以十进制方式显示,并且在那一点上出现了循环小数的问题,因此这种额外的复杂性和运行时成本对于BigDecimal的典型用例是不合适的。

底线是,在BigDecimal的典型用例中,这种额外的复杂性和运行时成本是不合适的。这包括财务计算,在这种情况下,会计准则不允许您使用循环小数。


1 - 这是一所非常好的小学。你可能在高中学过这些知识。

2 - 要么尝试消除除数和被除数的公共因数(计算代价昂贵),要么允许它们无限增长(空间使用成本昂贵,对后续操作的计算代价昂贵)。


BigDecimal 如何检测无限循环小数?它必须追踪并检测被除数中的循环。它可以选择另一种方式来处理,例如标记重复部分的位置等。 - polygenelubricants
@Stephen:非常感谢您对如何检测无限扩展的调查。我在发表评论后也去做了,但我认为我应该给你一个回答的机会 =) - polygenelubricants

10

这个类是BigDecimal而不是BigFractional。从你的一些评论中听起来,你只想抱怨有人没有将所有可能的数字处理算法集成到这个类中。金融应用程序不需要无限小数精度;只需要完全准确的值以所需精度为单位(通常是0、2、4或5个小数位)。

实际上我处理过许多使用double的金融应用程序。虽然我不喜欢这样做,但那就是它们编写的方式(也不是在Java中)。当存在汇率和单位转换时,可能会出现四舍五入和溢出问题。 BigDecimal消除了后者,但仍存在除法的前者。


2
+1。你对“BigDecimal而不是BigFractional”的理解很到位。并且你对于在金融应用中使用double的见解也很受赞赏。 - polygenelubricants

6
如果您想使用小数而不是有理数,并且需要在最终舍入之前进行精确算术运算(如舍入至美分或其他),这里有一个小技巧。
您可以始终操作您的公式,以便只有一个最终除法。这样,在计算过程中您就不会失去精度,并且总能得到正确四舍五入的结果。例如:
a/b + c

等于

(a + bc) / b.

1
哦,所有的工程师都在做什么呢?在这个时代,我们不得不为计算机进行手动操作时,我们应该感到不安。 - Pacerier

5

顺便说一句,我真的很希望能从与金融软件合作过的人那里得到见解。 我经常听到BigDecimal比double更好。

在财务报告中,我们始终使用scale = 2和 ROUND_HALF_UP的BigDecimal,因为报告中的所有打印值都必须导致可重复的结果。 如果有人使用简单计算器检查此内容,则需要如此。

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


3
您应该优先选择BigDecimal用于金融计算。舍入应由业务规定。例如,一个金额(100,00美元)必须平均分配到三个帐户中。必须有一条业务规则来确定哪个帐户需要多余的一分钱。
Double和float不适合在金融应用程序中使用,因为它们无法准确表示不是2的指数的1的分数。例如,考虑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的主要用例,因此您将找不到它。

2

有必要吗?

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

resulting in

b==1;

在金融系统中,你可能不需要这样做。在金融系统中,计算时会定义采用何种舍入方式和保留几位小数。在某些情况下,法律规定了舍入方式和保留的小数位数。所有组件都可以依赖于这种定义好的行为。如果返回 b==1,则会导致失败,因为它没有满足指定的行为。这在计算价格等方面非常重要。

这就像 IEEE 754 规范中用于表示二进制浮点数。一个组件不能通过优化“更好”的表示方式来减少信息损失,因为这会破坏契约。


double 是否符合金融行业的合同要求?您是否也无法控制 double 的舍入模式等内容? - polygenelubricants
1
在处理必须平衡的数量时,执行除法的正确方法是保持商和余数。将1.00美元分给七个人会发现每个人有0.14美元,再加上两个额外的便士,可以根据某些政策进行分配。 - supercat

2
为了进行除法运算并保留精度,您需要设置MATHcontext
例如:BigDecimal bd = new BigDecimal(12.12, MathContext.DECIMAL32).divide(new BigDecimal(2)).setScale(2, RoundingMode.HALF_UP);

1

我承认Java在表示分数方面的支持不是很好,但你必须意识到,在使用计算机时,完全保持精度是不可能的。至少在这种情况下,异常告诉你正在失去精度。

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

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

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

在Java中表示分数的最佳方法是什么?


@Pacerier - 不完全是这样;考虑无理数。 - Adrian Mouat
对于无理数,库将以表达式的形式存储数字。例如,sqrt 2被存储为sqrt 2,而不是1.4142135.....等等。这正是我们在高中数学中所做的;如果预先解析会给我们不精确的结果,那么我们只有在需要显示它时才解析数字的最后一步。计算机没有理由不能这样做。 - Pacerier
@Pacerier 如果给予足够的时间、CPU 和内存,它只是过于低效(虽然你可能可以部分归咎于计算机架构)。如果你愿意,可以尝试一下;在那之前我不会责怪库。 - Adrian Mouat
BigInteger与原始int相比也“远远不够高效”。这就是代码库的全部意义:编写“远远不够高效”的代码,当使用情况可以为功能而牺牲速度时,人们会使用它。 “远远不够高效”不是反对构建专用BigRealNumber库的理由。 - Pacerier
我支持这两种观点。一方面,即使过去20年了,Java仍然没有提供BigRealNumber类型的空间。另一方面,许多其他编程语言也是如此。 - Pacerier
显示剩余2条评论

0
注意我们正在使用计算机... 计算机有很多内存,精度需要内存。所以当你想要无限精度时,你需要
(infinite * infinite) ^ (infinite * Integer.MAX_VALUE) 兆字节的内存...

我知道 1 / 30.333333...,应该可以像"三分之一"这样将其存储在内存中,然后再将其乘回来,就应该得到 1。但我不认为Java有这样的东西...
也许你必须赢得诺贝尔奖才能写出这样的东西。 ;-)


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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接