让我们来看这样一个例子:
double a = 2.3F;
System.out.println(a);
输出的 a 为 2.299999952316284
不再是 2.3 了!
明明 float 小,double 大,我把小的数放到大的空间里面,为什么还会有“精度损失”?
关键点是表示精度而非“空间”大小:
-
浮点数的存储机制(IEEE 754 标准):
float
和double
都是基于 IEEE 754 标准存储浮点数。它无法精确存储所有的十进制小数,原因在于 二进制表示系统的局限性。float
使用 32 位,其中 1 位用于符号,8 位用于指数,23 位用于尾数(也叫有效位数)。double
使用 64 位,其中 1 位用于符号,11 位用于指数,52 位用于尾数。
因此,
float
的尾数只能存储 23 位有效数字,而double
的尾数可以存储 52 位有效数字。 -
浮点数在二进制中的表示限制:
浮点数通常不能被精确表示为二进制数。例如,2.3
是一个十进制数,但在二进制中它无法被精确表示,只能是一个近似值。- 当你在 Java 中声明
float a = 2.3F;
时,Java 会将2.3
转换成其在float
类型中的二进制近似值,这个近似值是2.2999999523162841796875
(其二进制表示)。 - 由于
float
的精度只有 23 位尾数,所以它只能表示到这么精确的值,即我们看到的四舍五入结果为2.3
。
- 当你在 Java 中声明
-
从
float
到double
的转换:
当你将float
转换为double
时,不会发生“精度损失”,只是double
能够表示得更加精确,它可以保留float
的原始二进制表示,但仍然是float
精度的近似值(这是因为它来自float
的有限精度的二进制近似)。float
的精度是 23 位尾数,即它的二进制近似值是2.2999999523162841796875
。- 当这个值转换为
double
时,double
可以表示更多的位数(52 位尾数),于是它就显示出了更多原始float
的二进制近似值的真实数字,即2.299999952316284
。
-
为什么看起来像是“损失精度”?
并不是你在double
中“损失了精度”,而是由于float
本身的精度有限,它只能表示出一个有限的近似值。这个近似值在被转换为double
时,double
可以显示更多的二进制位数,于是你看到的是float
原本就不够精确的数字。换句话说,当
float
被转化为double
时,并没有损失任何精度,只是double
展现了float
精度限制下的原本“隐藏的”部分。
总结:
float a = 2.3F;
:由于float
的精度限制,它存储的近似值是2.2999999523162841796875
,但显示为2.3
。double a = 2.3F;
:当float
的二进制近似值被存储到double
时,double
能显示出更多的尾数位数,展示了原本float
类型未能显示的那部分,即2.299999952316284
。
补充具体说明:
计算机在底层使用二进制存储数值,但并非所有的十进制小数都可以用二进制精确表示。这与我们用十进制表示某些分数(如 1/3
)时需要无限循环小数类似。
float
在 Java 中是一个 32 位的浮点类型,它能够精确存储的数的范围主要是有限的离散值。
在十进制系统中,数字 2.3
的分数表示是 23/10
。当我们将这个十进制数转换为二进制时,它无法表示为一个精确的有限二进制小数,因为:
- 二进制中只能精确表示某些分数,比如
1/2
,1/4
,1/8
等,都是 2 的幂。 - 而
2.3
并不是可以通过这些简单的 2 的幂表示出来的,因此它只能近似表示为一个有限的二进制数。
具体来说,2.3 转换为二进制后的无限小数大致是:
2.3 ≈ 10.0100110011001100110011...(无限循环)
由于计算机的位数有限,它无法存储无限位数的小数,因此只能截断,导致了精度误差。
对于像 1.2345678
这样的数字,float
可以精确表示。
但是对于像 2.3
这样的数字,它不能被精确表示,因为它无法在有限的位数内表示该小数的二进制表示。
如果需要表示精度更高的数字,应该使用 double
类型,它使用 64 位存储浮点数,拥有更大的尾数位数(52 位),可以表示大约 15 位十进制有效数字。
但是,即便使用 double
,并不是所有的十进制数仍然都可以精确表示,因为这是二进制系统的固有限制。
如果需要更高的精度,应考虑使用 double
或 BigDecimal
来表示和计算精确的十进制值。