CodeGym /课程 /JAVA 25 SELF /精度问题与特殊值

精度问题与特殊值

JAVA 25 SELF
第 6 级 , 课程 3
可用

1. 引言

看起来,如果计算机“聪明”,那么 0.1 + 0.2 应该就是 0.3。但事实并非如此。让我们用一个简单的例子来看看。


double x = 0.1;
double y = 0.2;
double sum = x + y;
System.out.println(sum); // 程序会输出什么?
浮点数相加:我们期望 0.3,实际是多少?

现在试着与 0.3 比较:

System.out.println(sum == 0.3); // 这里会是 true 还是 false?

如果你看到 false,不要惊讶!

原因在于内存中的数值表示

计算机以二进制表示数字。但并不是所有十进制小数都能表示为有限的二进制小数,就像 1/3 无法被十进制小数精确表示(0.333...)一样。比如,0.1 在二进制中是无限小数,为了存储不得不进行“舍入”。

通俗地说,double 有时“看起来”在精确存储你的数,但实际上只存储了一个非常接近的近似值。

2. 使用 double 进行运算会出现哪些“怪现象”?

我们来看一些实际示例。

示例 1:经典的 0.1 + 0.2 “魔法”

double a = 0.1;
double b = 0.2;
double sum = a + b;
System.out.println(sum);            // 0.30000000000000004
System.out.println(sum == 0.3);     // false

计算机输出的不是 0.3,而是 0.30000000000000004。差异很小,但如果你处理的是金融之类的场景——这就很关键了。

示例 2:迭代相加

double result = 0;
for (int i = 0; i < 10; i++)
{
    result += 0.1;
}
System.out.println(result); // 0.9999999999999999

本来想得到 1.0 —— 却得到略小的值。原因仍然是 double 内部的舍入。

为什么在真实任务中很重要

很多人会想:“差一点点有什么关系!”我们来看一个支付领域的例子。

假设你的网银要把 100 笔每笔 0.1 欧元的交易相加。如果你的程序在每次迭代中“丢失”十万分之一欧元,在银行这种规模下你就会“丢掉”真金白银。此时会计立刻就会来问你:“我们的钱去哪儿了?!”

3. 如何正确比较浮点数

由于 double 往往无法存下你期望的精确值,直接用 == 比较可能会误导。通常的做法是比较两个数之差的绝对值是否小于某个非常小的数(epsilon)。

带容差的比较示例

double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 0.000001;

if (Math.abs(a - b) < epsilon)
{
    System.out.println("几乎相等!"); // 这样比较更安全
}

这里我们的意思是:“如果两个数的差小于百万分之一,就认为它们相等。”
说明: 函数 Math.abs(value) 返回传入数字的绝对值(模)。

4. double 的特殊值:InfinityNaN-Infinity

类型 double 不仅能表示普通数字,还能表示特殊值。它们出现在那些数学老师会严禁你去做的场景里。

无穷大(Infinity

1 除以 0 会怎样?

double result = 1.0 / 0.0;
System.out.println(result); // Infinity

在 Java(以及许多语言)中,double 的除以 0 并不会抛出异常!相反,结果会变成一个特殊值“正无穷大”。

负无穷大(-Infinity

如果用负数除以 0,就会得到负无穷大:

double result = -1.0 / 0.0;
System.out.println(result); // -Infinity

“非数”(NaN — Not a Number)

如果做一些很“奇怪”的事,例如对负数求平方根:

double result = Math.sqrt(-1);
System.out.println(result); // NaN

或者 0.0 / 0.0 的结果:

double result = 0.0 / 0.0;
System.out.println(result); // NaN

NaN 表示“在现实世界中不被视为数的任何东西”。

如何检查特殊值

Java 提供了用于检查这些特殊值的函数:

System.out.println(Double.isInfinite(result));    // 若为无穷大则为 true
System.out.println(Double.isNaN(result));         // 若为 NaN 则为 true

表格:double 对非常规操作的反应

操作 结果 double 中存的是什么
1.0 / 0.0
Infinity +∞
-1.0 / 0.0
-Infinity -∞
0.0 / 0.0
NaN 非数
Math.sqrt(-1)
NaN 非数

5. 处理浮点数时的常见错误

错误 №1:通过 == 比较小数
最常见的陷阱:试图用普通比较来判断两个计算得到的浮点数是否相等。由于舍入误差的累积,你几乎总会得到意外结果。务必使用带容差的比较(epsilon)。

错误 №2:计算中意外出现 NaNInfinity
如果不控制除以零或对负数开方,程序中可能出现 NaNInfinity,并“感染”后续计算。别忘了使用 Double.isNaN()Double.isInfinite() 检查可疑值——这能帮助避免不愉快的惊喜。

错误 №3:把 NaN 用作“标记”
有些新手会用 NaN 作为“特殊”标记,比如“找不到就返回 NaN”。但请记住:NaN 很狡猾,用 == 比较不起作用!只能通过专用方法检查。

错误 №4:指望除以 0.0 会抛出异常
与整数除法不同,double 的除以 0.0 不会报错,而是返回 InfinityNaN。这可能导致静默且难以察觉的 bug——确实有结果,但与预期不符。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION