Java:无法获得双精度浮点数的精确和

mutmk8jj  于 2023-04-28  发布在  Java
关注(0)|答案(2)|浏览(177)

我需要对从字符串解析中获得的两个或多个double求和。我验证了双精度的正确解释。假设我有两个双精度数D1 = 12。69和D2 = 15。99,我希望总和是D1+D2 = 28。68,但sum实际上是由大量的十进制数字(十个或更多)组成的。我怎样才能得到只有两位小数的总和?
有人建议在其他一些帖子中使用BigDecimals,但我不清楚如何使用这样的对象来获得期望的结果。

a8jjtwal

a8jjtwal1#

我尝试了parseDoubleDecimalFormat,它能够提供我预期的答案。

src

package org.example;

import java.math.RoundingMode;
import java.text.DecimalFormat;

import static java.lang.Double.parseDouble;

public class JavaExercise {

    public JavaExercise() {
        System.out.println("Hi");
    }

    Double addDouble(String a, String b){
        DecimalFormat df = new DecimalFormat("#.##");
        df.setRoundingMode(RoundingMode.FLOOR);
        Double sum = parseDouble(a)+parseDouble(b);
        return  parseDouble(df.format(sum));
    }
}

测试(通过)

package org.example;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class JavaExerciseTest {

    JavaExercise javaExercise;

    @BeforeEach
    void init(){
        javaExercise = new JavaExercise();
    }

    @Test
    public void testDouble(){
        Double actual = javaExercise.addDouble("12.69", "15.99");
        Double expected = 28.68;
        assertEquals(expected, actual);
    }

    @Test
    public void testLongDouble(){
        Double actual = javaExercise.addDouble("12.690990", "15.909990");
        Double expected = 28.60;
        assertEquals(expected, actual);
    }
}
qacovj5a

qacovj5a2#

这个答案分为三个部分:

  • 解释为什么双精度是不准确的,为什么这是不容易修复的。
  • 策略,* 通常 * 的结果在'正确'的答案与双重数学,并会在您的情况下,但没有特别的保证
  • 一个关于如何使用BigDecimal的解释,它确实可以让你得到保证,而代价是其他问题

double不准确

拿一张纸。
写下1除以5的结果。用十进制形式写下来。你要写“0。2“把它还给我。
到目前为止,一切顺利。
现在做同样的事情,但这次,写下1除以3。
你会发现这是不可能的你写0。333333...最终你用完了纸上的空间来写更多的三。你把它还给我,数字有点不准确。
double相同的东西。但计算机是十进制的。1/5适合而1/3不适合的原因是因为我们是用十进制(基数为10)来写的,而5“很适合”10,但3不适合。基于同样的原因,你可以写下1/2(0。5 -因为2适合10)。解释为什么1/4适合稍微有点问题(4不能很好地适合10,但是一旦你写下适合的部分,也就是1/5,剩下1/20,20可以很好地适合10)-但同样的原则适用。
计算机以2为基数计数。这很烦人--而能很好地划分成10的东西是1,2,5,10,而任何可以分解成这些数字的因子的东西,以2为基数,它就是 * 1和2。所以1/2,1/4,1/8,5/16 -这些都是“完美”的,但即使是像0这样简单的东西。1(1/10)不适合。
见证人:

double v = 0.0;
for (int i = 0; i < 10; i++) v += 0.1;
System.out.println(v);
System.out.println(1.0 == v);

如果你不知道这个四舍五入业务,上面的结果会令人惊讶。它不打印1。0(而不是0。9999999),并且它也不打印'true' -相反,它打印false。这很奇怪.10 * 0.1显然是1。0.
你在这里被杀了。这就像我让你在一张纸条上写上“1/3”,再做两次,然后让你把这三张纸条上的数字加起来--这已经不是1了。0,而不是0。999999999(不是无限的,只是很多,而是有限的9),不等于1。0.
换一种完全不同的方式来思考:double由64位表示。
假设我给了你3个开关,你存储信息的唯一方法是让开关处于向上或向下状态。你可以“记住”8个独特的事件:000、001、010、011、100、101、110和111。就是这样。和doubles一样,但不是8(2的3次方),而是一个巨大的数字:2的64次方。
但这仍然是你能记住的唯一的东西,在0和1之间有无限数量的数字,更不用说-和+无穷之间,这就是double或多或少过度简化所能表示的。
double实际上只能表示略少于2^64个唯一数。让我们称之为“受祝福”的数字。对于任何未被祝福的数字,double math默默地舍入到最近的祝福数字。幸运的数字不是均匀分布的:在1.0附近有很多,当你远离1时。0的人越来越少。

如何使用double?

记住他们有轻微的错误。这表现在三个方面:

  • 打印时,请始终使用String.format或类似格式。

如果我将上面的代码稍微修改为:

double v = 0.0;
for (int i = 0; i < 10; i++) v += 0.1;
System.out.println(String.format("%.3f", v));

它打印1。000,因为我们拥有的双倍(0.999999999999999764左右)确实舍入到1。000如果我们明确地说:请最多保留3位小数。
总是不正确的,只是打印一个双精度,任何地方,除非你明确地希望呈现错误。相反,在渲染时始终使用一些舍入。
在比较时,请记住错误,并使用“增量比较”。与其问“ab相等”,不如问“ab之间的绝对差很小吗?”’。这是如何:

double EPSILON = 0.0000001;
double v = 0.0;
for (int i = 0; i < 10; i++) v += 0.1;
System.out.println(Math.abs(v - 1.0) < EPSILON);

这将打印所需的true
但是,你仍然在玩弄事实,有不准确的地方。

完美和BigDecimal

BigDecimal完美地以十进制表示数字。缺点是,它们非常慢。幸运的是,计算机的速度非常快,所以这通常不重要。
它们也有数学本身的缺点:如果你试图在BigDecimal数学中用1除以3,除非你告诉它舍入,否则它会抛出一个异常。...让你再次圆了。至少它的控制,但仍然。

BigDecimal a = new BigDecimal("12.69");
BigDecimal b = new BigDecimal("15.99");
BigDecimal c = a.add(b);
System.out.println(c);

通常,如果你试图建模具有不同原子单位的东西,最好是将这些原子存储在long中。例如,在大多数情况下,存储货币的最佳方式是美分。存储账单总计2篇文章,其中一篇花费15欧元。99欧元,另一个12欧元。六十九:

long priceA = 1599;
long priceB = 1269;
System.out.println("Bill total: €" + formatCents(priceA + priceB));

工作很好,其中formatCents来自库或如果需要手写:

public String formatCents(long v) {
  boolean sign = v >= 0;
  if (!sign) v = -v;
  int wholes = v / 100;
  int parts = v % 100;
  return String.format("%s%d.%02d", sign ? " " : "-", wholes, parts);
}

每种货币都有原子单位。日元只是日元,美元和欧元有美分,甚至比特币也有satoshis。

相关问题