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

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

相关推荐
组合缺一1 小时前
Solon Cloud Gateway 开发:Helloword
java·gateway·solon
老苏畅谈运维2 小时前
MySQL性能分析的“秘密武器”,深度剖析SQL问题
数据库·sql·mysql
金融OG2 小时前
99.16 金融难点通俗解释:营业总收入
大数据·数据库·python·机器学习·金融
Java诗人DK3 小时前
windows 安装 mysql 教程
数据库·windows·mysql
loser~曹4 小时前
Redis实现,分布式Session共享
数据库·redis·分布式
奕辰杰4 小时前
关于使用微服务的注意要点总结
java·微服务·架构
m0_748230215 小时前
适用于IntelliJ IDEA 2024.1.2部署Tomcat的完整方法,以及笔者踩的坑,避免高血压,保姆级教程
java·tomcat·intellij-idea
六毛的毛5 小时前
java后端之登录认证
java·开发语言·python
患得患失9496 小时前
【Django DRF Apps】【文件上传】【断点上传】从零搭建一个普通文件上传,断点续传的App应用
数据库·后端·django·sqlite·大文件上传·断点上传
customer087 小时前
【开源免费】基于SpringBoot+Vue.JS校园失物招领系统(JAVA毕业设计)
java·vue.js·spring boot·后端·开源