在Java中将双精度数分为整数和小数部分的最佳方法是什么?

12

我已经尝试使用以下方法将5.6(例如)分离:

private static double[] method(double d)
{
    int integerPart = 0;
    double fractionPart = 0.0;
    integerPart = (int) d;
    fractionPart = d - integerPart;
    return new double[]{integerPart, fractionPart};
}

但是我得到的是:

[0] = 5.0
[1] = 0.5999999999999996

你有任何建议可以在不将数字转换为字符串的情况下完成这个任务吗?


更新了我的答案,提供另一个建议 :-) - aioobe
5个回答

12

使用 BigDecimal 来进行相同的计算。(使用 double 会因其表示方式而产生精度问题)

  • 通过 new BigDecimal(String.valueOf(yourDouble)) 构造它(这仍然是通过字符串,但部件不是通过字符串操作分隔的)
  • 使用 bd.subtract(new BigDecimal(bd.intValue()) 来确定小数部分

为什么不直接使用 new BigDecimal(double val) 构造函数呢? - Swati
因为: @Swati: groovy:000> new BigDecimal(5.6) - new BigDecimal(5.0) ===> 0.5999999999999996447286321199499070644378662109375 (这不是减法的问题,而是当5.6被转换为BigDecimal时引入的误差) - Nathan Hughes
1
@Swati:double 是基于二进制的,decimal 是基于十进制的。小数部分是因为 5.6 十进制无法在二进制中精确表示。如果你使用 double 构造十进制数,则不准确性已经被引入了。 - Paul Ruane
应该使用String.valueOf,但是因为这个好主意加1分。 - asgs
@Swati - 因为你没有利用BigDecimal的精度。这是Josh Bloch在某个API陷阱的演示中的一部分。不要使用double构造函数。 - Bozho

5

这里有另一个基于BigDecimal的解决方案(不经过String)。

private static double[] method(double d) {
    BigDecimal bd = new BigDecimal(d);
    return new double[] { bd.intValue(),
                          bd.remainder(BigDecimal.ONE).doubleValue() };
}

如您所见,即使是小数部分,您仍然不会得到仅为0.6的输出。(甚至无法将0.6存储在double中!)这是因为数学上的实数5.6实际上并非精确地表示为一个double类型的数值,而是表示为5.599999...。
您也可以进行以下操作:
private static double[] method(double d) {
    BigDecimal bd = BigDecimal.valueOf(d);
    return new double[] { bd.intValue(),
                          bd.remainder(BigDecimal.ONE).doubleValue() };
}

实际上返回的是[5.0, 0.6]

BigDecimal.valueOf 在大多数JDK中(内部)通过调用Double.toString来实现。但至少与字符串相关的内容不会使您的代码混乱 :-)


评论中提出了一个好的后续问题:

如果它表示为5.599999999...,那么为什么Double.toString(5.6)恰好给出"5.6"

Double.toString方法实际上非常复杂。从Double.toString文档中可以看到:

[...]

对于m或a的小数部分必须打印多少个数字?必须至少有一个数字来表示小数部分,并且除此之外还需要尽可能多但只有这么多位数字,以便唯一地区分类型double的相邻值和参数值。也就是说,假设x是由该方法为有限非零参数d生成的十进制表示所表示的精确数学值。那么d必须是最接近x的double值;或者如果两个double值对x的距离相等,则d必须是它们之一,并且d的有效数字的最低位必须为0。

[...]

获取字符"5.6"的代码归结为FloatingDecimal.getChars

private int getChars(char[] result) {
    assert nDigits <= 19 : nDigits; // generous bound on size of nDigits
    int i = 0;
    if (isNegative) { result[0] = '-'; i = 1; }
    if (isExceptional) {
        System.arraycopy(digits, 0, result, i, nDigits);
        i += nDigits;
    } else {
        if (decExponent > 0 && decExponent < 8) {
            // print digits.digits.
            int charLength = Math.min(nDigits, decExponent);
            System.arraycopy(digits, 0, result, i, charLength);
            i += charLength;
            if (charLength < decExponent) {
                charLength = decExponent-charLength;
                System.arraycopy(zero, 0, result, i, charLength);
                i += charLength;
                result[i++] = '.';
                result[i++] = '0';
            } else {
                result[i++] = '.';
                if (charLength < nDigits) {
                    int t = nDigits - charLength;
                    System.arraycopy(digits, charLength, result, i, t);
                    i += t;
                } else {
                    result[i++] = '0';
                }
            }
        } else if (decExponent <=0 && decExponent > -3) {
            result[i++] = '0';
            result[i++] = '.';
            if (decExponent != 0) {
                System.arraycopy(zero, 0, result, i, -decExponent);
                i -= decExponent;
            }
            System.arraycopy(digits, 0, result, i, nDigits);
            i += nDigits;
        } else {
            result[i++] = digits[0];
            result[i++] = '.';
            if (nDigits > 1) {
                System.arraycopy(digits, 1, result, i, nDigits-1);
                i += nDigits-1;
            } else {
                result[i++] = '0';
            }
            result[i++] = 'E';
            int e;
            if (decExponent <= 0) {
                result[i++] = '-';
                e = -decExponent+1;
            } else {
                e = decExponent-1;
            }
            // decExponent has 1, 2, or 3, digits
            if (e <= 9) {
                result[i++] = (char)(e+'0');
            } else if (e <= 99) {
                result[i++] = (char)(e/10 +'0');
                result[i++] = (char)(e%10 + '0');
            } else {
                result[i++] = (char)(e/100+'0');
                e %= 100;
                result[i++] = (char)(e/10+'0');
                result[i++] = (char)(e%10 + '0');
            }
        }
    }
    return i;
}

如果它被表示为5.599999999...,那么为什么System.out.println(Double.toString(5.6));会精确地输出5.6 - Eng.Fouad
那真是个好问题。Double.toString(5.6) 真的很复杂。请查看文档。(简单来说:它不会尝试打印 double 的精确值,而是打印最接近表示值的最简单值,而不是任何其他值。) - aioobe
"" + d 被翻译为 String.valueOf(d),它又调用了 Double.toString(..)(或者编译器直接翻译成 D.toString())。 - Bozho
好的,我只是采取了懒人的方式 ;) - aioobe
1
如果有人感兴趣,我为此创建了一个后续问题(https://dev59.com/VVfUa4cB1Zd3GeqPHWCP)。 - aioobe

1

为了了解发生了什么,请查看数字的二进制表示:

double d = 5.6;
System.err.printf("%016x%n", Double.doubleToLongBits(d));
double[] parts = method(d);
System.err.printf("%016x %016x%n",
                  Double.doubleToLongBits(parts[0]),
                  Double.doubleToLongBits(parts[1]));

输出:

4016666666666666
4014000000000000 3fe3333333333330

5.6 是 1.4 * 22,但 0.6 是 1.2 * 2-1。由于它具有较低的指数,规范化导致尾数向左移动三位。原来的循环项(..66666..)是分数7/5的近似值,现在已经被遗忘,缺失的位被替换为零。

给定原始的double值作为输入到您的方法中,无法避免这种情况。要保留精确值,您需要使用表示所需值的格式,例如 Apache commons-math 中的 Fraction。(对于此特定示例,使用 d=5.6BigDecimal 也可以准确表示它,但它不能准确表示其他数字,例如 4/3)


0

穷人解决方案(使用字符串)

    static double[] sp(double d) {
        String str = String.format(Locale.US, "%f", d);
        int i = str.indexOf('.');
        return new double[] {
            Double.parseDouble(str.substring(0, i)),
            Double.parseDouble(str.substring(i))
        };
    }

(本地化,以便我们真正获得小数


-1

将double转化为字符串:

String doubleAsString = Double.toString(123.456);

获取小数点前的字符串:

String beforeDecimal=doubleAsString.substring(0,doubleAsString.indexOf(".")); //123

获取小数点后的字符串:

String afterDecimal=doubleAsString.substring(doubleAsString.indexOf(".")+1); //456


你为什么要回答一个三年前的问题?而且一个好的答案需要更多的解释。 - Dinistro

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