float vs decimal

引子

起因是有群友在尝试解决 double 不够存数据的问题,看到了 decimal,但对 decimal 的理解还是不对。

这段话说的很含糊,可能是从 ai 的文本中截取的一段。而问为什么要用 decimal 时,这也是一般人的回答。

清晰的解释

浮点数在计算机中以二进制形式存储,而很多十进制小数(例如 0.1)在二进制下表示为无限循环小数。由于浮点数的存储空间有限,这些无限二进制小数必须被截断,从而引入精度误差。[1]相比之下,decimal 类型使用十进制存储,可以精确表示十进制小数,因此不会产生类似的精度问题,适合对精度要求高的场景(如财务计算)。[2]

进制与截断

decimal 如其名一样是十进制的,而一般的浮点(float/double)是基于二进制(binary floating-point)遵循 IEEE 754 标准的。[3]

最常见的情况是 0.1 + 0.2 = 0.3 的例子,请参考以下代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
checkFiniteInBinary("0.1");
checkFiniteInBinary("0.2");
checkFiniteInBinary("0.3");
checkFiniteInBinary("0.5");

float floatResult = .1f + .2f;
System.out.printf("floatResult = %.64f\n", floatResult);

double doubleResult = .1 + .2;
System.out.printf("doubleResult = %.64f\n", doubleResult);

BigDecimal decimalResult = new BigDecimal("0.1").add(new BigDecimal("0.2"));
System.out.printf("decimalResult = %.64f\n", decimalResult);

doubleResult = .2 + .3;
System.out.printf("doubleResult = %.64f\n", doubleResult);
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
19:53:38: Executing ':org.ryuu.Main.main()'…

Starting Gradle Daemon...
Gradle Daemon started in 1 s 634 ms
> Task :compileJava UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE

> Task :org.ryuu.Main.main()
十进制数0.1在二进制数下不能有限表示。
0.0001100110011001100110011001100110011001100110011001100110011001... (仅显示64位)

十进制数0.2在二进制数下不能有限表示。
0.0011001100110011001100110011001100110011001100110011001100110011... (仅显示64位)

十进制数0.3在二进制数下不能有限表示。
0.0100110011001100110011001100110011001100110011001100110011001100... (仅显示64位)

十进制数0.5在二进制数下能有限表示。
0.1

floatResult = 0.3000000119209289600000000000000000000000000000000000000000000000
doubleResult = 0.3000000000000000400000000000000000000000000000000000000000000000
decimalResult = 0.3000000000000000000000000000000000000000000000000000000000000000
doubleResult = 0.5000000000000000000000000000000000000000000000000000000000000000

BUILD SUCCESSFUL in 6s
2 actionable tasks: 1 executed, 1 up-to-date
19:53:46: Execution finished ':org.ryuu.Main.main()'.

0.1,0.2这样的数在二进制里类似十进制的1/3(0.3333333…),是无限循环的。浮点数会将数据截断,因此会丢失精度。[4]

1
2
0.1(decimal) = 0.0001100110011001100110011001100110011001100110011001100110011001... (binary)
0.2(decimal) = 0.0011001100110011001100110011001100110011001100110011001100110011... (binary)

另请参阅

github 有完整的示例工程


  1. David Goldberg 的经典论文《What Every Computer Scientist Should Know About Floating-Point Arithmetic》说明,有限位表示只能近似覆盖无限多实数,因此多数实数运算结果无法在固定 bit 数中精确表示;其中也专门用 0.1 解释了十进制有限小数在二进制中的不可精确表示。 ↩︎

  2. Microsoft C# 官方文档《Floating-point numeric types》把 decimal 定位为适合金融、货币、利率等由小数点右侧位数决定精度的场景,并说明 0.1 可由 decimal 精确表示但不能由 doublefloat 精确表示。Java 的 BigDecimal 官方 API 也将其定义为由任意精度整数 unscaled value 和 32-bit scale 组成的十进制数值类型,见 Oracle Java SE 17《BigDecimal》。 ↩︎

  3. IEEE Standards Association 的《IEEE 754-2019 - IEEE Standard for Floating-Point Arithmetic》是浮点算术标准入口,说明该标准规定了计算机编程环境中的二进制和十进制浮点交换格式、算术格式和方法。 ↩︎

  4. Goldberg 论文同样指出,0.1 虽然有有限十进制表示,但在二进制中是无限循环表示,因此在以 2 为基数的有限精度浮点中不能被精确表示。 ↩︎