为什么浮点小数在被截断并转换为字符串后仍会重新出现?

问题描述 投票:0回答:1

我需要在变量中存储一个带有n位小数的浮点数,但这似乎太复杂了。

如果我运行这段代码:

float testFloat = -18.479999542236328f;
System.out.println(getFloatAsStringIWithNDecimals(testFloat, 9));
System.out.println(testFloat + "");
System.out.println(getFloatAsStringIWithNDecimals(testFloat, 3));
System.out.println(getFloatWithNDecimals(testFloat, 3));
System.out.println(Float.parseFloat(getFloatAsStringIWithNDecimals(testFloat, 3)));
System.out.println(getFloatAsStringIWithNDecimals(Float.parseFloat(getFloatAsStringIWithNDecimals(testFloat, 3)), 9));
System.out.println(getFloatWithNDecimals(Float.parseFloat(getFloatAsStringIWithNDecimals(testFloat, 3)), 3));
System.out.println(getFloatAsStringIWithNDecimals(getFloatWithNDecimals(Float.parseFloat(getFloatAsStringIWithNDecimals(testFloat, 3)), 2), 9));

public static String getFloatAsStringIWithNDecimals(float f, int nDecimals) {
    return String.format(Locale.US, "%.0"+nDecimals+"f", f);
}

public static float getFloatWithNDecimals(float f, int nDecimals) {
    int multiplier = 1;
    for(int i = 1; i <= nDecimals; i++) {
        multiplier *= 10;
    }

    return ( (int)(f * multiplier) ) *1f / multiplier;
}

public static float getFloatWithNDecimals(float f) {
    return getFloatWithNDecimals(f, 2);
}

这是输出:

-18.479999542
-18.48
-18.480
-18.48
-18.48
-18.479999542
-18.48
-18.479999542

为什么这些输出,即使我截断小数,将它们转换为 int,然后除以 100,即使我通过使用

String.format
将浮点数转换为 String 来截断它们?

谢谢

java floating-point
1个回答
0
投票

我只需要在变量中存储一个带有n位小数的浮点数,但它看起来太复杂了......

这不是

float
可以存储的内容。您的问题类似于:“我只想将“hello”一词存储在
int
变量中,但看起来太复杂了”。或者“我想将高尔夫球杆放入保龄球包中,但它不合适”。

浮点数以固定精度存储数字,但以二进制形式存储。

拿一张纸。去写1/20吧。没问题。 “0.05”。

现在做同样的事情并在上面写上1/15。 0.066666666666666...哦天哪。你做不到。没有一张信纸足够大,无法准确地写下来。

1/20

有效而
1/15
无效的原因是你以10为基数编写它,而10的素因数是5和2。如果你的分数的分母仅分解为素因数5和2,那么它就会“起作用”(20 是 
2*2*5
 - 这就是五和二,所以可以起作用),如果其中甚至有一个非 5、非 2 值,那么它就不起作用; 15 是 
5*3
 - 那里有一个 3,所以它不起作用。

在二进制中,唯一的约数是

2。所以分母需要分解成任意数量的 2。

这是另一个简单的示例,您确实应该编写并运行它,以便充分理解以下概念:

float

double
 本质上是不精确的,永远不能舍入为浮点数或双精度数,如果您尝试,您将失败了。

double v = 0.0; for (int i = 0; i < 10; i++) v+= .1; System.out.println(v == 1.0);
这将打印

false

,这是非常令人惊讶的。从什么时候开始十倍哦点一不等于一了?但事实并非如此,因为 10 作为分母是 
5*2
,其中包含一个非 2,所以虽然十分之一在十进制表示法中可以很好地工作 (
0.1
),但在二进制表示法中却不行 - 它就像 
0.3333333....

round(1/3 to 10 decimal places) * 3

的结果不是1.0(是
0.9999999999
),
round(1/10 to 10 binarial places) * 10
的结果也不是1.0。

现在尝试相同的特技,但将

0.1

 替换为 
0.125
(这是八分之一,这确实有效:8 分解为 
2*2*2
 - 这都是二,所以在浮点/双精度数学中是精确的),确实八乘以八分之一确实等于 1.0。

既然您知道了,您就知道“四舍五入到 8 位”是对

float

/
double
 的无意义操作。他们根本做不到 - 因为你的意思是“8 位
十进制表示法”。

那我该怎么办?

您可以使用 BigDecimal,它确实可以十进制工作,但很难使用并且非常慢,并且难以除以(要求 BD 将 1 除以 3 会抛出异常,除非您告诉它如何舍入,原因与您将要舍入的原因相同)如果我要求你在一张纸上写下三分之一,“抛出异常”——如果不引入一些错误,这是不可能的)。

更好的选择是在渲染时进行四舍五入

- 每当该数字转换为字符串时,对然后进行四舍五入。例如: double v = 5; v = v / 3.0; System.out.printf(".4f", v);

在打印操作期间
将您的数字四舍五入为 1.6667,效果非常好。如果您不想将此字符串直接发送到标准输出,则可以使用

String.format。任何指定“返回四舍五入的数值”的方法都不能将 double

float 作为返回类型。它需要有 BigDecimal
String
或者,如果您有一个设定的固定点,您想要对其进行操作,例如“我想存储欧分,但不想存储半分,所以总是四舍五入到两位小数”,然后存储最小单位在
long
。因此,不要为 1 欧元和 23 欧分的东西存储 
double priceOfThisThing = 1.23;

。而是存储

long priceInCents = 123;

旁注:
float
没用

double
更快、更好、更强。没有理由使用 float ,除非 [A] 你要创建一个非常大的数组(超过 ~100k 个元素!),或者 [B] 某些规范明确要求 IEEE32 
float

数学。这是极其罕见的。或 [C] 最终运行此代码的平台可能是一个 32 位芯片,它也以 32 位进行浮点数学运算。在撰写本文时(2024 年),这些芯片极为罕见。

是的,
double
是“更大”。您可能会认为这意味着它们“较慢”。除非您拥有“真正”的奇特硬件,否则这是不正确的;芯片在其设计运行的尺寸上运行速度更快。对于现代芯片(不到十年前制造的任何芯片)来说,这是 
double

,而不是

float

换句话说,float
正确的几率实际上为零。
	

© www.soinside.com 2019 - 2024. All rights reserved.