金额存储类型-浮点数的精度丢失问题

在涉及到支付金额项目中,都需要对金额在数据库中进行存储,最近使用到了MySQL数据库的decimal 类型对金额进行存储,同时 Java 程序中使用了 BigDecimal 类进行存放计算

我们常使用的 int, long 整数类型只能存储整数,而对于金额涉及到小数的情况则无法存储

所以存储金额不能采用整数,而应该采用能存储小数的类型

浮点数存储格式

float 和 double 类型存在精度问题,浮点数在表示小数时并不是像整型一样直接存储的,而是通过IEEE 754标准存储,以一种二进制的科学计数法来表示:

s 表示符号位,浮点数为负数时s为1,浮点数为正数时 s 为 0

M 为尾数部分,float 类型尾数部分为 23bit,double 类型尾数部分为 52bit

E 为阶码部分,float 类型阶码部分为 8bit,double 类型阶码部分为 11bit

为什么不使用 float 和 double 类型

我们知道了浮点数每一个部分的位数都有长度的限制,但是使用以上格式存储的浮点数如果长度超过了对应的限制,则会进行截断,数据的准确性就不能得到保证了

float浮点数0.1的二进制存储表示

例如以浮点数 0.1 表示二进制小数是如何存储的

首先十进制整数转二进制是采用的辗转相除法,通过对原数一直除 2 取余,然后将结果倒叙排列得到二进制表示

十进制小数转二进制则是通过对原数一直乘 2 取整,然后将结果正叙排列得到二进制表示

复制代码
(0)  0.1 × 2 = 0 + 0.2
(1)  0.2 × 2 = 0 + 0.4
(2)  0.4 × 2 = 0 + 0.8
(3)  0.8 × 2 = 1 + 0.6
(4)  0.6 × 2 = 1 + 0.2
(5)  0.2 × 2 = 0 + 0.4
(6)  0.4 × 2 = 0 + 0.8
(7)  0.8 × 2 = 1 + 0.6
(8)  0.6 × 2 = 1 + 0.2
(9)  0.2 × 2 = 0 + 0.4
(10)  0.4 × 2 = 0 + 0.8
(11)  0.8 × 2 = 1 + 0.6
(12)  0.6 × 2 = 1 + 0.2
(13)  0.2 × 2 = 0 + 0.4
(14)  0.4 × 2 = 0 + 0.8
(15)  0.8 × 2 = 1 + 0.6
(16)  0.6 × 2 = 1 + 0.2
(17)  0.2 × 2 = 0 + 0.4
(18)  0.4 × 2 = 0 + 0.8
(19)  0.8 × 2 = 1 + 0.6
(20)  0.6 × 2 = 1 + 0.2
(21)  0.2 × 2 = 0 + 0.4
(22)  0.4 × 2 = 0 + 0.8
(23)  0.8 × 2 = 1 + 0.6
(24)  0.6 × 2 = 1 + 0.2
......

这里只获取了 25 位长度的二进制,并没有获取 0.1 表示的完整二进制数字,所以 0.1 二进制前 25位表示为:

使用 IEEE 标准存储为:

  • s = 0
  • M = 0001100110011001100110011
  • E = 127

对于浮点数而言,尾数M超过对应限制则会将超过部分截断并舍入

float 表示的 M 截断完为:00011001100110011001100

需要再对 M 进行舍入,舍入的方式有以下四种:

  • 偶数舍入:向最接近的偶数舍入
  • 0舍入:向数轴零方向舍入,即直接截尾
  • 朝正无穷舍入:向数轴的正无穷方向舍入
  • 朝负无穷舍入:向数轴的负无穷方向舍入

所以使用 IEEE 标准表示的尾数 M 为 00011001100110011001100,而对于后面部分的精度则被舍弃,这时候如果存在一个浮点数的二进制表示前 23 位和 0.1 一致,则计算机判定两个浮点数是相等的,就出现了精度丢失的问题

我们知道了 float 浮点数在二进制尾数部分超过了 23 位则会出现精度丢失问题,同样 double 浮点数在二进制尾数部分超过了 52 位则同样也会出现精度丢失问题

BigDecimal类

这时候就引入了Java中的 BigDecimal类,BigDecimal 类底层是采取的字符数组进行存储的,对于大数的存储计算非常方便

Java 在 java.math 包中提供的 API 类 BigDecimal 可以对大数进行运算,例如加减乘除等

java 复制代码
BigDecimal bigDecimal1 = new BigDecimal("10222222222222.1");
BigDecimal bigDecimal2 = new BigDecimal("100000000000.1");

System.out.println(bigDecimal1.add(bigDecimal2));    //10222222222222.1 + 100000000000.1 = 10322222222222.2
System.out.println(bigDecimal1.subtract(bigDecimal2));   //10222222222222.1 - 100000000000.1 = 10122222222222.0
System.out.println(bigDecimal1.multiply(bigDecimal2));   //10222222222222.1 * 100000000000.1 = 1022222222223232222222222.21
System.out.println(bigDecimal1.divide(bigDecimal2, RoundingMode.DOWN));   //10222222222222.1 / 100000000000.1 = 102.2

涉及到的精度四舍五入方法主要有:

java 复制代码
bigDecimal1.setScale(2, RoundingMode.DOWN);   //舍去多余部分小数
bigDecimal1.setScale(2, RoundingMode.UP);   //进位
bigDecimal1.setScale(2, RoundingMode.HALF_DOWN);    //四舍五入,碰到5舍弃
bigDecimal1.setScale(2, RoundingMode.HALF_UP);    //四舍五入,碰到5进位

涉及到的比较大小的方式:

java 复制代码
System.out.println(bigDecimal1.compareTo(bigDecimal2));  //1,表示 bigDecimal1 > bigDecimal2
System.out.println(bigDecimal1.compareTo(bigDecimal2));  //-1,表示 bigDecimal1 < bigDecimal2
System.out.println(BigDecimal.ZERO.compareTo(BigDecimal.ZERO));  //0,表示 0 == 0 

BigDecimal 也可以转换为基本数据类型:

java 复制代码
int i = bigDecimal1.intValue();
long l = bigDecimal1.longValue();
float f = bigDecimal1.floatValue();
double d = bigDecimal1.doubleValue();

注意

BigDecimal 最好采用字符串形式初始化,如果使用浮点型初始化则不能避免精度丢失问题

decimal类型

在 MySQL 中可以使用 decimal(m, n) 存储金额数据

m:指定小数点左边和右边可以存储的十进制数字的最大个数最大精度 38

n:指定小数点右边可以存储的十进制数字的最大个数。小数位数必须是从 0 到 a 之间的值。默认小数位数是 0,涉及金额一般是定义 n = 2

例如初始化表格时使用的建表语句

java 复制代码
CREATE TABLE table (
    id INT AUTO_INCREMENT PRIMARY KEY,
    amount DECIMAL(18, 2) DEFAULT '0.00'
);

建表时直接默认金额为 0.00 可在计算时相对于 NULL 减少数据转换的问题

最后

所以在Java中涉及到支付金额相关的计算时优先采用 BigDecimal 类,在数据库中存储金额字段时 decimal 是最佳的选择,不仅避免了浮点数的精度丢失问题,同时对于数据量也有更大的灵活性

相关推荐
数字化顾问32 分钟前
Flink ProcessFunction 与低层级 Join 实战手册:实时画像秒级更新系统
java·开发语言
计算机学姐39 分钟前
基于微信小程序的垃圾分类管理系统【2026最新】
java·vue.js·spring boot·mysql·微信小程序·小程序·mybatis
掘金安东尼1 小时前
深入 Neo4j:从图数据库原理到企业知识引擎的实践指南
数据库·neo4j
Mr_hwt_1231 小时前
spring boot框架中本地缓存@Cacheable原理与踩坑点详细解析
java·spring boot·后端·缓存
TiAmo zhang1 小时前
SQL Server 2019实验 │ 管理SQL Server的安全性
数据库·sqlserver
DokiDoki之父1 小时前
数据库—数据库设计 & 多表查询 & 事务
数据库·sql
zl9798991 小时前
SpringBoot-自动配置原理
java·spring boot·spring
兮动人1 小时前
Java 单元测试中的 Mockito 使用详解与实战指南
java·开发语言·单元测试
豆沙沙包?1 小时前
2025年--Lc186--64. 最小路径和(多维动态规划,矩阵)--Java版
java·矩阵·动态规划
武子康1 小时前
Java-151 深入浅出 MongoDB 索引详解 性能优化:慢查询分析 索引调优 快速定位并解决慢查询
java·开发语言·数据库·sql·mongodb·性能优化·nosql