在处理项目金额数据时,虽然 double 日常可用,但无法精确计算金额,BigDecimal 更适合。尽管部分旧项目可能使用 double,但在高精度要求下仍推荐 BigDecimal。
Java 中的 BigDecimal 和 double
1. double
1.1. 无法精确表示部分十进制小数
十进制小数转换为二进制时,部分数值会变成无限循环小数,而 double 只能存储有限位数的二进制,因此会截断并保留近似值。
最经典的例子:
double a = 0.1;
double b = 0.2;
System.out.println(a + b);
原因:0.1 转二进制是 0.0001100110011...(无限循环),double 只能存储其近似值,叠加后误差被放大。
1.2. 能精确表示的情况(特例)
- 整数:在
2^53 范围内的整数(约 ±9×10¹⁵),double 可以精确存储(例如 25、20.0 等)。例:25 * 0.8 = 20.0,结果是整数,因此 double 能精确表示。
- 可被 2 的幂次整除的小数:例如
0.5(二进制 0.1)、0.25(二进制 0.01)等。
1.3. 适用场景与风险场景
适合使用 double 的场景
- 科学计算 / 工程计算:允许微小误差(如物理模型、图形渲染、数据统计等)。
- 非精确小数场景:如温度(
23.5℃)、体重(62.3kg)等,对精度要求不高的数值。
- 需要大范围数值:当数值范围超过整数类型(如
long 的上限 ±9×10¹⁸),且可接受近似值时。
禁止使用 double 的场景(精度敏感)
- 金融 / 支付相关:金额(如
100.05 元)、税率、手续费等,误差可能导致资金损失或对账不平。
- 业务关键小数:如商品单价、折扣率、库存数量(带小数,如
1.5 件)等,需精确记录和计算。
- 数据库字段映射:若数据库用
NUMERIC/DECIMAL(精确小数类型),实体类用 double 接收会导致精度丢失。
1.4. 常见问题与解决方案
1.4.1. 问题:double 打印时显示很多小数位(如 0.1 显示为 0.1000000000000001)
- 原因:
double 存储的是近似值,打印时会显示其真实存储的二进制对应的十进制值。
- 解决:用
String.format() 或 DecimalFormat 格式化输出(仅用于展示,不改变存储的近似值):
double num = 0.1;
System.out.println(String.format("%.1f", num));
1.4.2. 问题:double 比较大小(如 a == b 失效)
- 原因:近似值导致看似相等的数值,实际存储的二进制不同(如
0.1 + 0.2 与 0.3)。
- 用
BigDecimal 比较(推荐):a.compareTo(b) == 0(相等返回 0)。
- 若必须用
double,则比较两者差值的绝对值是否小于极小值(如 1e-9):
解决:
double a = 0.1 + 0.2;
double b = 0.3;
if (Math.abs(a - b) < 1e-9) {
System.out.println("相等");
}
1.5. 总结
double 是一把'双刃剑':它的优势是范围大、计算效率高,适合非精确场景;但精度丢失的特性使其在核心业务(尤其是金融)中完全不可用。
核心原则:
- 若业务允许微小误差 → 用
double;
- 若要求绝对精确 → 必须用
BigDecimal + 数据库 NUMERIC 类型。
2. BigDecimal
BigDecimal 的内部存储基于两部分:
- 一个任意精度的整数'unscaled value'(未缩放值)
- 一个 32 位整数'scale'(缩放因子,表示小数点的位置)
例如:
- 当你输入
new BigDecimal("123.45") 时,它会存储为 unscaled value=12345,scale=2
- 当你输入
new BigDecimal("123.4500") 时,同样存储为 unscaled value=1234500,scale=4
这意味着:
- 如果你通过字符串构造
BigDecimal,它会精确保留字符串中的所有数字和小数点位置
- 如果你通过浮点数(如 double)构造,可能会因为浮点数本身的精度问题导致意外结果
BigDecimal 会保留尾部的零,这一点与 double 不同
示例代码:
import java.math.BigDecimal;
public class BigDecimalExample {
public static void main(String[] args) {
BigDecimal bd1 = new BigDecimal("100.00");
BigDecimal bd2 = new BigDecimal("100");
BigDecimal bd3 = new BigDecimal(100.00);
System.out.println(bd1);
System.out.println(bd2);
System.out.println(bd3);
}
}
所以,如果你希望精确控制 BigDecimal 的数字形式,最佳实践是:
- 使用字符串作为构造参数
- 需要时使用
setScale() 方法显式设置精度
- 避免使用 double 作为构造参数
SQL 中的 DECIMAL 和 NUMERIC
在数据库中,DECIMAL 和 NUMERIC 是用于存储精确数值的类型,主要用于需要高精度计算的场景(如金融、货币、税务等),它们的特性和使用方式非常相似,但存在一些细微差别。
1. 基本特性
两者都是精确数值类型,与 FLOAT 或 DOUBLE(近似数值类型)不同,它们不会丢失精度,因为它们以字符串形式存储数值(而非二进制浮点数)。
定义格式通常为:
DECIMAL(p, s) 或 NUMERIC(p, s)
p(precision):总位数(整数部分 + 小数部分),范围通常为 1~65(不同数据库可能有差异)。
s(scale):小数部分的位数,范围为 0~p。
2. 主要区别
标准 SQL 中对两者的定义有细微差异:
NUMERIC(p, s):严格遵守 p 和 s 的限制,插入的值必须完全符合指定的精度和小数位数(超出则报错)。
DECIMAL(p, s):可以接受比指定精度更高的值,但会自动截断或四舍五入到指定的 s 位小数(具体行为取决于数据库实现)。
但实际中,大多数数据库(如 MySQL、PostgreSQL、SQL Server)将两者视为同义词,处理方式完全一致,没有区别。例如:
- MySQL 文档明确说明
DECIMAL 和 NUMERIC 是相同的。
- PostgreSQL 中两者行为一致,仅名称不同。
3. 使用场景
适合存储需要精确计算的数值,例如:
- 货币金额(如
DECIMAL(10, 2) 表示最大 99999999.99)。
- 税率、利息等需要固定小数位的数值。
不适合存储不需要高精度的大数值(如科学计算中的近似值),此时用 FLOAT 更节省空间。
4. 与 Java 中 BigDecimal 的配合
数据库的 DECIMAL/NUMERIC 类型在 Java 中通常映射为 BigDecimal 类型,两者都是精确数值类型,配合使用可以避免精度丢失问题。
示例(JDBC 读取):
PreparedStatement ps = connection.prepareStatement("SELECT price FROM products WHERE id = ?");
ps.setInt(1, productId);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
BigDecimal price = rs.getBigDecimal("price");
}
总结
DECIMAL 和 NUMERIC 在大多数数据库中功能完全相同,都用于存储精确数值。
- 核心是通过
(p, s) 控制总位数和小数位数,确保数值精度。
- 与 Java 的
BigDecimal 配合使用,是处理金融等高精度场景的最佳实践。