将双精度浮点数中的小数点向左移动

98

我有一个double类型的变量等于1234,我想把小数点向左移动一位变成12.34

为了做到这一点,我需要将1234乘以0.1两次,就像这样:

double x = 1234;
for(int i=1;i<=2;i++)
{
  x = x*.1;
}
System.out.println(x);

这将打印出结果,"12.340000000000002"。

有没有一种方法可以在不仅仅格式化为两位小数的情况下,正确地存储双精度数值 12.34 呢?


44
你为什么不执行 x /= 100; 这个操作呢? - Mark Ingram
还是 x *= 0.01; - user207421
9个回答

192
如果您使用的是doublefloat,则应该进行四舍五入或者预期会看到一些舍入误差。如果您无法做到这一点,请使用BigDecimal。 您遇到的问题是0.1不是一个精确的表示,在进行两次计算时,您正在累积该误差。 然而,100可以准确地表示,因此请尝试:
double x = 1234;
x /= 100;
System.out.println(x);

输出:

12.34
这是因为Double.toString(d)会在您的代表下进行少量舍入,但并不多。如果您想知道没有舍入可能会看起来怎样:
System.out.println(new BigDecimal(0.1));
System.out.println(new BigDecimal(x));

输出:

0.100000000000000005551115123125782702118158340454101562
12.339999999999999857891452847979962825775146484375

简而言之,在浮点数中,无论您是否显式地这样做,合理的答案都不可避免地需要进行四舍五入。


注意:x / 100x * 0.01在舍入误差方面并不完全相同。这是因为第一个表达式的舍入误差取决于x的值,而第二个表达式中的0.01具有固定的舍入误差。

for(int i=0;i<200;i++) {
    double d1 = (double) i / 100;
    double d2 = i * 0.01;
    if (d1 != d2)
        System.out.println(d1 + " != "+d2);
}

打印

0.35 != 0.35000000000000003
0.41 != 0.41000000000000003
0.47 != 0.47000000000000003
0.57 != 0.5700000000000001
0.69 != 0.6900000000000001
0.7 != 0.7000000000000001
0.82 != 0.8200000000000001
0.83 != 0.8300000000000001
0.94 != 0.9400000000000001
0.95 != 0.9500000000000001
1.13 != 1.1300000000000001
1.14 != 1.1400000000000001
1.15 != 1.1500000000000001
1.38 != 1.3800000000000001
1.39 != 1.3900000000000001
1.4 != 1.4000000000000001
1.63 != 1.6300000000000001
1.64 != 1.6400000000000001
1.65 != 1.6500000000000001
1.66 != 1.6600000000000001
1.88 != 1.8800000000000001
1.89 != 1.8900000000000001
1.9 != 1.9000000000000001
1.91 != 1.9100000000000001

注意:这与您的系统随机性(或电源供应)无关。这是由于表示误差造成的,其将每次产生相同的结果。双精度double的精度受限且基于2进制而非10进制,因此在十进制中可以准确表示的数字通常无法在二进制中准确表示。


26
我简直不敢相信我一开始居然没有想到那么做!谢谢 :-P - BlackCow
6
虽然100可以在二进制格式中精确表示,但是除以100却无法精确表示。因此,像你所做的那样写“1234/100”并不能真正解决潜在的问题——它应该完全等同于写“1234*0.01”。 - Brooks Moses
1
@Peter Lawrey:您能否更详细地解释一下为什么数字是奇数还是偶数会影响四舍五入吗?我认为/=100和*=.01应该是相同的,因为即使100是int类型,由于类型强制转换的结果,它也将被转换为100.0。 - eremzeit
1
/100*0.01 是等价的,但不等于 OP 的 *0.1*0.1 - Amadan
1
我想说的是,平均而言,两次乘以0.1会引入比一次乘以0.01更大的误差;但我很乐意承认@JasperBekkers关于100是不同的、完全可表示为二进制的观点。 - Amadan
显示剩余7条评论

52

不行 - 如果你想精确地存储十进制数值,请使用 BigDecimal。与 double 一样,它根本就不能准确地表示像 0.1 这样的数字,就像你无法用有限数量的小数位准确表示三分之一的值一样。


46

如果只是格式问题,尝试使用printf。

double x = 1234;
for(int i=1;i<=2;i++)
{
  x = x*.1;
}
System.out.printf("%.2f",x);

输出

12.34

8
更高评级的答案在技术上更有洞察力,但这是对原帖问题的正确回答。我们通常不太关心double的轻微不精确性,因此BigDecimal会过于复杂,但在显示输出时,我们经常希望确保输出与我们的直觉相符,所以 System.out.printf() 是正确的方法。 - dimo414

31

在金融软件中,使用整数代表分是很常见的。在学校里,我们学习了如何使用定点而不是浮点数,但通常会使用2的幂次方。将分存储在整数中也可以称为“定点”。

int i=1234;
printf("%d.%02d\r\n",i/100,i%100);

在课堂上,我们被问及在何种进制下可以准确表示哪些数字。

对于 base=p1^n1*p2^n2...,你可以表示任何N,其中N=n*p1^m1*p2^m2。

base=14=2^1*7^1 为例...你可以表示 1/7、1/14、1/28、1/49,但不包括 1/3。

我知道一些关于金融软件的事情——我曾将Ticketmaster的财务报告从VAX汇编语言转换为PASCAL。他们有自己的formatln()函数,可以处理分数部分。转换的原因是32位整数已经不够用了。+/- 20亿个“pennies”相当于2千万美元,在世界杯或奥运会上,这已经超出了范围,我忘了。

我被要求保密。 在学术界,如果研究成果好,就会发表;在工业界,你必须保守秘密。


12

你可以尝试使用整数数字表示

int i =1234;
int q = i /100;
int r = i % 100;

System.out.printf("%d.%02d",q, r);

5
@Dan: 为什么?这是金融应用程序(或任何其他应用程序,在其中即使有微小的舍入误差也是不可接受的)的正确方法,同时仍保持硬件级速度。(当然,通常情况下,它会被包裹在一个类中,而不是每次都写出来)。 - Amadan
7
这个解决方案有一个小问题 - 如果余数 r 小于10,则不会进行零填充,而1204将产生12.4的结果。正确的格式化字符串更类似于“%d.%02d”。 - jakebman

10

这是由计算机存储浮点数的方式引起的。它们并不完全准确。作为程序员,你应该阅读这个浮点指南,熟悉处理浮点数所面临的困境和挑战。


啊,我刚刚正在写一个解释,链接到完全相同的地方。+1。 - Pops
@Lord Haha,抱歉。我还是被Skeet了。 :-) - CanSpice
我明白这就是原因,但我想知道是否有一些创造性的方法可以移动小数点?因为在double中可以干净地存储12.34,只是它不喜欢乘以0.1。 - BlackCow
1
如果在double中可以干净地存储12.34,你不认为Java会这样做吗?但事实并非如此。您需要使用其他数据类型(例如BigDecimal)。另外,为什么不直接除以100而不是循环执行呢? - CanSpice
哦...是的,将它除以100会得到一个干净的12.34...谢谢 :-P - BlackCow
更准确地说,它们被存储为最接近的数字(1+(n/223))(2k),其中n和k是整数,且0 <= n < 223。还有更多细节,但都不相关。 - Aaron

10
有趣的是,许多帖子都提到要使用BigDecimal,但没有人打算基于BigDecimal给出正确的答案?因为即使使用BigDecimal,你仍然可能会出错,就像这段代码演示的一样。

有趣的是,许多帖子提到了使用BigDecimal,但没有人提供基于BigDecimal的正确答案。这是因为即使使用BigDecimal,你仍然可能会出错,正如这段代码所演示的那样。

String numstr = "1234";
System.out.println(new BigDecimal(numstr).movePointLeft(2));
System.out.println(new BigDecimal(numstr).multiply(new BigDecimal(0.01)));
System.out.println(new BigDecimal(numstr).multiply(new BigDecimal("0.01")));
给出这个输出。
12.34
12.34000000000000025687785232264559454051777720451354980468750
12.34

BigDecimal的构造函数明确提到,与数值构造函数相比,使用字符串构造函数更好。最终精度也受可选的MathContext影响。

根据BigDecimal Javadoc的说法,只要使用字符串构造函数,就可以创建一个完全等于0.1的BigDecimal。


5
是的,确实存在这个问题。每次进行双精度操作都可能会丢失精度,但是每种操作丢失的精度数量不同,并且可以通过选择正确的操作顺序来最小化丢失的精度。例如,在乘法运算时,最好按指数对数字集进行排序后再进行乘法运算。
任何一本关于数字计算的好书都会描述这一点。例如: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html 回答您的问题: 使用除法而不是乘法,这样您就可以得到正确的结果。
double x = 1234;
for(int i=1;i<=2;i++)
{
  x =  x / 10.0;
}
System.out.println(x);

3
不,就像Java浮点类型(实际上所有浮点类型)一样,它们是大小和精度之间的权衡。虽然它们对许多任务非常有用,但如果需要任意精度,则应使用BigDecimal

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