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