如何克服Java中的不准确性

当我执行以下程序时,我开始了解准确性问题

public static void main(String args[]) { double table[][] = new double[5][4]; int i, j; for(i = 0, j = 0; i <= 90; i+= 15) { if(i == 15 || i == 75) continue; table[j][0] = i; double theta = StrictMath.toRadians((double)i); table[j][1] = StrictMath.sin(theta); table[j][2] = StrictMath.cos(theta); table[j++][3] = StrictMath.tan(theta); } System.out.println("angle#sin#cos#tan"); for(i = 0; i < table.length; i++){ for(j = 0; j < table[i].length; j++) System.out.print(table[i][j] + "\t"); System.out.println(); } } 

输出是:

 angle#sin#cos#tan 0.0 0.0 1.0 0.0 30.0 0.49999999999999994 0.8660254037844387 0.5773502691896257 45.0 0.7071067811865475 0.7071067811865476 0.9999999999999999 60.0 0.8660254037844386 0.5000000000000001 1.7320508075688767 90.0 1.0 6.123233995736766E-17 1.633123935319537E16 

(请原谅无组织的输出)。 我注意到了几件事:

  • sin 30即0.5存储为0.49999999999999994
  • tan 45即1.0存储为0.9999999999999999
  • tan 90即infinityundefined存储为1.633123935319537E16 (这是一个非常大的数字)。

当然,我很难看到输出(甚至在解密输出后)。

所以我读过这篇文章,最好的答案告诉我:

这些准确性问题是由浮点数或点数的内部表示引起的,并且您无法避免它。

顺便说一句,在运行时打印这些值通常仍会产生正确的结果,至少使用现代C ++编译器。 对于大多数操作而言,这不是什么大问题。

于2008年10月7日7:42回答

康拉德鲁道夫

所以,我的问题是:

有没有办法防止这种不准确的结果(在Java中)?

我应该完成结果吗? 在那种情况下,我将如何存储infinityDouble.POSITIVE_INFINITY

你必须对浮点数采取一些zen *方法:而不是消除错误,学会忍受它。

在实践中,这通常意味着做以下事情:

  • 显示数字时,使用String.format指定要显示的精度(它将为您进行适当的舍入)
  • 当与预期值进行比较时,不要寻找相等( == )。 相反,寻找一个足够小的delta: Math.abs(myValue - expectedValue) <= someSmallError

编辑 :对于无限,同样的原则适用,但有一个调整:你必须选择一些数字“足够大”作为无限。 这也是因为你必须学会​​接受而不是解决不精确的价值观。 在像tan(90度)这样的情况下,double不能以无限精度存储π/ 2,所以你的输入是非常接近但不完全是90度的东西 - 因此,结果非常接近大,但不是无限。 您可能会问“为什么当你将最接近的双Double.POSITIVE_INFINITY传递给π/ 2时它们Double.POSITIVE_INFINITY返回Double.POSITIVE_INFINITY ”,但这可能会导致歧义:如果你真的想要那个数字的棕褐色,而不是90度呢? 或者,如果(由于先前的浮点错误)你有一些比最接近的可能值稍微远离π/ 2的东西,但是对于你的需要它仍然是π/ 2? JDK不是为你做出任意决定,而是以面值处理你接近但不完全π/ 2的数字,从而给你一个很大但不是无穷大的结果。

对于某些操作,特别是那些与money有关的操作,你可以使用BigDecimal来消除浮点错误:你可以真正表示像0.1这样的值(而不是真正接近0.1的值,这是float或double可以做的最好的) 。 但这速度要慢得多,并且对sin / cos之类的东西没有帮助(至少对于内置库而言)。

*这可能实际上不是禅宗,而是在口语意义上

你必须使用BigDecimal而不是double 。 不幸的是, StrictMath不支持BigDecimal ,所以你必须使用另一个库,或者你自己的sin / cos / tan

这是使用任何语言的浮点数所固有的。 实际上,使用具有固定最大精度的任何表示都是固有的。

有几种解决方案。 一种是使用扩展精度数学包 – 通常建议Java使用BigDecimalBigDecimal可以处理更多的精度数字,并且 – 因为它是十进制表示而不是二进制补码表示 – 倾向于以对于习惯在基数10中工作的人而言不那么令人惊讶的方式进行四舍五入。并不一定使它们更正确 ,请注意。二进制不能完全代表1/3,但也不能小数。)

还有扩展精度二进制补码浮点表示。 Java直接支持float和double(通常也由硬件支持),但是可以编写支持更多精度数字的版本。

当然,任何扩展精度包都会降低计算速度。 所以你不应该求助于它们,除非你真的需要它们。

另一种可能是使用定点二进制而不是浮点。 例如,大多数财务计算的标准解决方案只是根据最小货币单位计算 – 美国便士 – 整数,仅转换为显示格式(例如美元和美分) / O。 这也是Java中用于时间的方法 – 内部时钟报告整数毫秒(或纳秒,如果使用纳米级调用),这给出了足够的精度和足够实用的值范围目的。 同样,这意味着舍入倾向于以符合人类期望的方式发生……再次,这不是关于准确性而是关于不会让用户感到惊讶。 而这些表示,因为它们以整数或长整数进行处理,允许快速计算 – 实际上比浮点更快。

还有其他解决方案涉及计算有理数或其他变化,以试图在计算成本和精度之间进行折衷。

但我也要问……你真的需要比漂浮更精确吗? 我知道这种结果是令人惊讶的,但在很多情况下,让它发生是完全可以接受的,当你向用户显示结果时,可能会转向一个不太令人惊讶的小数量的数字。 在许多情况下,浮点数或双精度对于实际使用来说都很好。 这就是硬件支持它们的原因,这也是他们使用该语言的原因。