龙空技术网

聊一聊Java中的浮点精度问题

我喜欢思考 42

前言:

目前我们对“java浮点数”大体比较着重,同学们都想要剖析一些“java浮点数”的相关文章。那么小编在网络上搜集了一些有关“java浮点数””的相关文章,希望朋友们能喜欢,各位老铁们一起来学习一下吧!

1 Java 浮点数的奥秘

在 Java 编程语言中,浮点数类型包括 float 和 double。尽管这两种类型广为人知,但许多开发者可能无法解释下面这个看似简单的问题:

你准备好揭开这个谜团了吗?

2 深入了解 Float 和 Double

float 和 double 都用于表示浮点数,但它们在内存中占用的空间不同:float 占用 32 位,而 double 占用 64 位。这影响了它们的精度和表示范围。让我们通过一些示例来探究这一点:

double a = 0.1;double b = 0.2;double c = a + b;System.out.println("double value 0.1 + 0.2 = " + c); // 输出 0.30000000000000004float a1 = 0.15f;float result = a1 * 3;System.out.println("float value 0.15*3 is " + result); // 输出 0.45000002

输出的结果:

double value 0.1 + 0.2 = 0.30000000000000004float value 0.15*3 0.45000002

这些结果可能初看之下令人困惑,因为它们与我们预期的精确值不符。这种差异的原因在于浮点数的表示方式。

3 浮点数的真相:0.1 的 IEEE 754 转换

在 Java 中,float 和 double 类型的浮点数遵循 IEEE 754 标准,这个标准详细定义了如何使用 32 位(对于 float)和 64 位(对于 double)来表示浮点数。让我们通过一个例子来探索像 0.1 这样的十进制数是如何转换为二进制表示的。

3.1 十进制到二进制的转换

将十进制数 0.1 转换为二进制数,我们遵循以下步骤:

将 0.1 乘以 2,并记录整数部分。取小数部分,再次乘以 2,并记录整数部分。重复这个过程,直到小数部分为 0 或达到所需的精度。

对于 0.1,这个过程如下:

步骤

操作

整数部分

小数部分

1

0.1 * 2

0

0.2

2

0.2 * 2

0

0.4

3

0.4 * 2

0

0.8

4

0.8 * 2

1

0.6

5

0.6 * 2

1

0.2

6

0.2 * 2

0

0.4

这个过程会无限重复,因为 0.1 在二进制中是一个无限循环小数。

3.2 二进制到 IEEE 754 的转换

在 IEEE 754 标准中,一个浮点数被分为三个部分:

符号位(Sign):0 表示正数,1 表示负数。指数(Exponent):表示数值的范围,对于 float 是 127 的偏移量,对于 double 是 1023 的偏移量。尾数(Mantissa):表示数值的精度部分,不包括第一个 1(隐含的)。

对于 0.1 的二进制表示(0.0001100110011001100110011...),我们进行如下转换:

符号位:0(正数)指数:我们需要将二进制数转换为科学记数法形式,即 1.10011001100110011001101 * 2^-4。对于 float 类型,指数部分是 -4 + 127 = 123,即 01111011。尾数:我们取科学记数法中的小数部分 10011001100110011001101(忽略隐含的前导 1),并将其填充到 23 位(对于 float)。

最终的 IEEE 754 表示为:

符号位

指数

尾数

0

01111011

10011001100110011001101

请注意,由于尾数部分是截断的,这会导致精度损失,这就是为什么在 Java 中计算 0.1 + 0.2 不等于 0.3 的原因。这种精度损失是浮点数表示的固有特性,而不是 Java 特有的问题。

4 浮点数的精度问题:深入理解

在 Java 中使用 float 和 double 时,我们经常遇到精度问题,这主要是由于两个原因:

无限二进制表示的截断:某些十进制小数在二进制中是无限循环的,这导致在转换过程中丢失精度。IEEE 754 表示的限制:在转换为 IEEE 754 标准格式时,我们只能使用有限的位数来表示数值,这进一步限制了精度。4.1 验证精度损失

我们可以通过以下代码来验证 float 和 double 的精度损失:

jshell> System.out.println(new BigDecimal(0.1f));0.100000001490116119384765625
4.2 逆向工程 IEEE 表示

我们可以通过查看 float 的二进制表示来进一步理解这一点:

int floatBits = Float.floatToIntBits(0.1f);String binaryString = String.format("%32s", Integer.toBinaryString(floatBits)).replace(' ', '0');System.out.println("Float value: " + 0.1f);System.out.println("Binary representation: " + binaryString);System.out.println("Sign bit: " + binaryString.substring(0, 1));System.out.println("Exponent: " + binaryString.substring(1, 9));System.out.println("Mantissa: " + binaryString.substring(9));

正如预期的那样,它证实了我们的想法,输出如下:

Float value: 0.1Bianry representation: 00111101110011001100110011001101Sign bit: 0Exponent: 01111011Mantissa: 10011001100110011001101

这段代码展示了如何从 float 的二进制表示中提取符号位、指数和尾数。

5 理解并应用这些知识

了解这些精度问题后,我们可以得出以下结论:

避免在需要精确计算的场景中使用浮点数,如金融、医学或复杂科学计算。不要直接比较两个浮点数是否相等;相反,使用一个小的 delta 值来检查它们之间的差异。例如:

boolean isEqual = Math.abs(a - b) < 0.0000001;
使用 BigDecimal 或类似类进行精确计算6 结论

通过深入理解浮点数的表示和精度问题,我们可以更好地处理 Java 中的浮点数计算。记住,当涉及到精确值时,选择正确的数据类型和方法至关重要。希望现在你明白为什么 0.1 + 0.2 会返回 0.30000000000000004 而不是 0.3。这种差异是由于浮点数的表示和精度限制造成的。

标签: #java浮点数 #java浮点数比较