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

在涉及到支付金额项目中,都需要对金额在数据库中进行存储,最近使用到了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 小时前
Django中的软删除
数据库·django·sqlite
阿里嘎多哈基米3 小时前
SQL 层面行转列
数据库·sql·状态模式·mapper·行转列
抠脚学代码3 小时前
Ubuntu Qt x64平台搭建 arm64 编译套件
数据库·qt·ubuntu
皮皮林5513 小时前
SpringBoot 全局/局部双模式 Gzip 压缩实战:14MB GeoJSON 秒变 3MB
java·spring boot
jakeswang3 小时前
全解MySQL之死锁问题分析、事务隔离与锁机制的底层原理剖析
数据库·mysql
weixin_456904273 小时前
Spring Boot 用户管理系统
java·spring boot·后端
趁你还年轻_3 小时前
异步编程CompletionService
java
DKPT3 小时前
Java内存区域与内存溢出
java·开发语言·jvm·笔记·学习
sibylyue3 小时前
Guava中常用的工具类
java·guava
Heliotrope_Sun3 小时前
Redis
数据库·redis·缓存