【Java基础面试题043】BigDecimal为什么能保证精度不丢失?
回答重点
BigDecimal使用十进制来表示数值,而不是二进制浮点数表示法,这使得它能够精确地表示所有十进制数值,不需要任何转换或舍入。
而且BigDecimal是无限精度,可以表示任意精度的小数(受限于内存),因此不会动不动被舍入截断,也可以手动设置精度和舍入模式来控制计算的精度
BigDecimal内部使用两个字段存储数字,一个是整数部分intVal,另一个用来表示小数点的位置scale,避免了浮点数转化过程中可能的精度丢失
计算时通过整数计算,再结合小数点位置和设置的精度与舍入行为,控制结果精度,避免了有默认浮点数舍入导致的误差
简化版:
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal; // 存储整数部分
private final int scale; // 存储小数点的位置
public BigDecimal(String val) {
// 使用 BigInteger 来表示数值
intVal = new BigInteger(val.replace(".", ""));
scale = val.contains(".") ? val.length() - val.indexOf(".") - 1 : 0;
}
}
例如:
BigDecimal bigDecimal = new BigDecimal(11.123);
inVal存储11122999999999332 ...
scale存储49,因为小数后有49位
如果是另外两种创建对象的方式,经过断点调试,inVal的值是null,scale是3
扩展知识
浮点数精度丢失的原因
- 二进制浮点数表示:
- Java使用IEEE 754标准来表示浮点数,这个标准用二进制来表示小数
- 但有些十进制小数在二进制下是无限不循环小数(例如0.1在二进制中是0.0001100110011...),因此需要被截断或四舍五入。
- 有限的表示范围:
float
和double
都有固定的大小(float
是32位,double
是64位),这限制了它们能表示的精度和范围。有些数字在这些有限的位数内无法精确表示。
- 舍入误差:
- 由于浮点数的表示方式,进行算术运算时,可能会产生舍入误差。例如,
0.1 + 0.2
在理论上应该等于0.3
,但在浮点数运算中可能得到0.30000000000000004
。
- 由于浮点数的表示方式,进行算术运算时,可能会产生舍入误差。例如,
0.1 * 0.2
public class FloatPrecisionExample {
public static void main(String[] args) {
double a = 0.1;
double b = 0.2;
System.out.println(a * b); // 输出 0.020000000000000004
}
}
由于double底层是二进制来表示十进制小数,不过double并不能精确表示0.1,0.2,所以0.1 * 0.2自然就产生了微笑的误差
使用BigDecimal
import java.math.BigDecimal;
public class BigDecimalPrecisionExample {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
System.out.println(a.multiply(b)); // 输出 0.02
}
}
0.1 * 0.2底层大概是这么处理的:
0.1:intVal = 1 scale = 1
0.2:intVal = 2 scale = 1
先1 x 2 = 2
再标小数点位置2 -> 0.02