浮点数为什么会丢失精度?BigDecimal为什么不会?
前言
在java中,要保证小数运算精度不丢失我们得用BigDecimal对象。这篇文章就分析一下为什么用浮点数会造成精度丢失?BigDecimal是怎么解决精度丢失问题的?下面我们一起看看吧!
浮点数的表示
浮点数是如何在计算机里存储的?它不是简单地直接存储我们看到的小数,而是采用了一种叫IEEE 754标准的方式。 这个标准把数拆成三个部分:符号位(正负号)、指数部分(数字的规模)、尾数部分(具体数值)。问题的关键就在于——* 尾数的位数是有限的*,也就是说,有些小数(特别是十进制转二进制时)根本无法精确表示。
一个直观案例
来看看下面这段代码:
public class FloatPrecision {
public static void main(String[] args) {
double a = 0.1;
double b = 0.2;
double c = a + b;
System.out.println(c); // 输出 0.30000000000000004
}
}
理论上,0.1 加 0.2 应该等于 0.3,但结果却是 0.30000000000000004,这不是个特例,而是浮点数的一种“正常表现”。原因就在于,* 0.1 和 0.2 在二进制中无法精确表示*,只能存个近似值,因此计算结果也会出现误差。
小数转二进制的尴尬之处
要弄清楚为什么会这样,得从小数转二进制的原理说起。转化过程简单来说就是——
- 用2乘小数部分,取整部分作为二进制位。
- 剩下的小数部分继续乘2,如此反复。
举个例子:将0.1转成二进制:
- 0.1 × 2 = 0.2,取整数0,余下0.2;
- 0.2 × 2 = 0.4,取整数0,余下0.4;
- 0.4 × 2 = 0.8,取整数0,余下0.8;
- 0.8 × 2 = 1.6,取整数1,余下0.6;
- ……
这个过程是无限循环的,得到的是个无限二进制小数:0.00011001100110011……。但浮点数存储时只能取有限位数,这就导致了误差。
BigDecimal
的解决方案
为了应对这种精度丢失,Java 提供了一个更可靠的解决方案——BigDecimal
。BigDecimal
是个强大的类,它通过字符串构造器 或其他精确方式,避免了浮点数的近似表示。
关键的三个参数:

无标度值(Unscaled Value):这是一个整数,表示BigDecimal的实际数值。
标度(Scale):这是一个整数,表示小数点后的位数。
BigDecimal的实际数值计算公式为:unscaledValue × 10^(-scale)。
理解标度这一概念
BigDecimal是通过一个"无标度值"和一个"标度"来表示一个数的。 标度就是小数点可以移动的"距离":
- 正标度:小数点右移。例如
123.45
,无标度值为12345
,标度为2
,即12345 × 10^{-2}
,得到123.45
。 - 零标度:无小数点。例如无标度值
12345
,标度为0
,结果就是12345
。 - 负标度:小数点左移。例如无标度值
12345
,标度为-2
,即12345 × 10^2
,得到1234500
。
提示
负标度在需要处理非常大的整数时非常有用,可以使用负数的标度来指定小数点左侧的位数。这可以保持整数的精度而又不丢失尾部零位。
注意:不要用equals比较BigDecimal的数值是否相等
直接上源码吧:
public boolean equals(Object x) {
if (!(x instanceof BigDecimal)) return false;
BigDecimal xDec = (BigDecimal) x;
if (x == this) return true;
// 比较标度
if (scale != xDec.scale) return false;
long s = this.intCompact;
long xs = xDec.intCompact;
if (s != INFLATED) {
if (xs == INFLATED)
xs = compactValFor(xDec.intVal);
return xs == s;
} else if (xs != INFLATED)
return xs == compactValFor(this.intVal);
return this.inflated().equals(xDec.inflated());
}
在使用BigDecimal
时,一个常见的坑就是误用equals
方法。这是因为equals
不仅比较数值,还比较标度。所以比较数值是否相同的话用 compareTo()
比较吧
import java.math.BigDecimal;
public class BigDecimalComparison {
public static void main(String[] args) {
BigDecimal num1 = new BigDecimal("1.0");
BigDecimal num2 = new BigDecimal("1.00");
System.out.println("equals: " + num1.equlse(num2)); // 输出false
System.out.println("compareTo: " + num1.compareTo(num2)); // 输出: 0
}
}
如上代码中,equals
返回false
,因为1.0
和1.00
的标度不同。正确的做法是使用compareTo
来比较,这样只会比较数值,不考虑标度。
小结
因为某些十进制的小数在二进制中是一个无限循环的小数,所有用浮点数存在进度丢失问题。BigDecimal
采用标度的形式解决了精度丢失问题。同时在使用 BigDecimal
的时候,使用String
的构造器。最后在使用BigDecimal
比较数值是否相同的时候不要使用equals
,请用compareTo
。