为何对float截断/转String后,小数部分仍会重新出现?
我需要在变量中存储保留n位小数的float类型数值,但操作过程中遇到了奇怪的精度问题。运行以下Java代码:
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将float转成String截断小数,最终都会出现上述输出,这是为什么?
原因解析
核心问题出在float类型的本质特性上:
float是二进制浮点数,无法精确表示大部分十进制小数
float基于IEEE 754标准,用32位二进制存储数值。像0.1、0.01这类十进制小数,在二进制里是无限循环的,只能被近似存储。你定义的-18.479999542236328f本身就不是精确值,float会把它近似为最接近的可表示二进制值,这个值对应的十进制就是-18.479999542。String.format只是格式化输出,并没有改变float的实际存储值
当你用String.format保留3位小数得到-18.480时,这只是把float的近似值做了格式化展示,底层的float变量存储的还是那个二进制近似值。一旦你把格式化后的字符串再转成float,得到的还是原来的近似值,所以再次打印9位小数时,还是会显示-18.479999542。截断转int的操作同样无法解决精度问题
你的getFloatWithNDecimals方法里,f * multiplier这一步已经因为float的精度问题产生了误差。比如-18.479999542 * 1000的结果并不是精确的-18479.999542,而是一个接近的二进制近似值,转int后再除以1000,得到的依然是原来的float近似值,所以最终输出还是-18.48(格式化后的展示),但底层存储值没变。float的toString()方法会自动做近似展示
当你直接打印testFloat + ""时,Java会调用float的toString()方法,这个方法会自动选择最短的十进制字符串来表示该float的近似值,所以显示-18.48,但这并不代表它的实际存储值就是精确的-18.48。
解决方案
如果需要精确控制小数位数,应该使用BigDecimal类型,它可以精确表示十进制小数,避免二进制浮点数的精度误差。或者如果只是用于展示,直接用String.format格式化输出即可,不要试图修改float的存储值——因为float本身就做不到精确存储指定的十进制小数。
内容的提问来源于stack exchange,提问作者user2342558

