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

在涉及到支付金额项目中,都需要对金额在数据库中进行存储,最近使用到了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 是最佳的选择,不仅避免了浮点数的精度丢失问题,同时对于数据量也有更大的灵活性

相关推荐
qq_5298353517 分钟前
ThreadLocal内存泄漏 强引用vs弱引用
java·开发语言·jvm
落笔画忧愁e22 分钟前
扣子Coze飞书多维表插件添加数据记录
java·服务器·飞书
不太可爱的叶某人44 分钟前
【学习笔记】MySQL技术内幕InnoDB存储引擎——第5章 索引与算法
笔记·学习·mysql
岁岁岁平安1 小时前
Redis基础学习(五大值数据类型的常用操作命令)
数据库·redis·学习·redis list·redis hash·redis set·redis string
量子联盟2 小时前
原创-基于 PHP 和 MySQL 的证书管理系统,免费开源
开发语言·mysql·php
小光学长3 小时前
基于vue框架的防疫科普网站0838x(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库
极限实验室3 小时前
使用 Docker Compose 简化 INFINI Console 与 Easysearch 环境搭建
数据库·docker·devops
秋千码途3 小时前
小架构step系列08:logback.xml的配置
xml·java·logback
飞翔的佩奇3 小时前
Java项目:基于SSM框架实现的旅游协会管理系统【ssm+B/S架构+源码+数据库+毕业论文】
java·数据库·mysql·毕业设计·ssm·旅游·jsp
时来天地皆同力.3 小时前
Java面试基础:概念
java·开发语言·jvm