以项目驱动学习,以实践检验真知
前言
很多系统都有「处理金额」的需求,比如电商系统、财务系统、收银系统,等等。只要和钱扯上关系,就不得不打起十二万分精神来对待,一分一毫都不能出错,否则对系统和用户来说都是灾难。
保证金额的准确性主要有两个方面:溢出和精度。溢出是指存储数据的空间得充足,不能金额较大就存储不下了。精度是指计算金额时不能有偏差,多一点少一点都不行。
溢出问题大家都知道如何解决,选择位数长的数值类型即可,即不用 float
用 double
。而精度问题,double
就无法解决了,因为浮点数会导致精度丢失。
我们来直观感受一下精度丢失:
double money = 1.0 - 0.9;
这个运算结果谁都知道该为 0.1
,然而实际结果却是 0.09999999999999998
。出现这个现象是因为计算机底层是二进制运算,而二进制并不能精准表示十进制小数。所以在商业计算等精确计算中要使用其他数据类型来保证精度不丢失,一定不要使用浮点数。
本螃蟹接下来会详细讲解在实际开发中到底该怎样进行商业计算,并将所有代码和 SQL 语句放在了 Github 上,克隆下来即可运行。
解决方案
有两种数据类型可以满足商业计算的需求,第一个自然是专为商业计算而设计的 Decimal 类型,第二个则是定长整数。
Decimal
关于数据类型的选择,一要考虑数据库,二要考虑编程语言。即数据库中用什么类型来存储数据,代码中用什么类型来处理数据。
数据库层面自然是用 decimal
类型,因为该类型不存在精度损失的情况,用它来进行商业计算再合适不过。
将字段定义为 decimal
的语法为 decimal(M,N)
,M
代表存储多少位,N
代表小数存储多少位。假设 decimal(20,2)
,则代表一共存储 20 位数值,其中小数占 2 位。
我们新建一张用户表,字段很简单就两个,主键和余额:
这里小数位置保留 2 点,代表金额只存储到分,实际项目中存储到什么单位得根据业务需求来定,都是可以的。
数据库层面搞定了咱们来看代码层面,在 Java 中对应数据库 decimal
的是 java.math.BigDecimal
类型,它自然也能保证精度完全准确。
要创建BigDecimal
主要有三种方法:
BigDecimal d1 = new BigDecimal(0.1); // BigDecimal(double val)
BigDecimal d2 = new BigDecimal("0.1"); // BigDecimal(String val)
BigDecimal d3 = BigDecimal.valueOf(0.1); // static BigDecimal valueOf(double val)
前面两个是构造函数,后面一个是静态方法。这三种方法都非常方便,但第一种方法禁止使用!看一下这三个对象各自的打印结果就知道为什么了:
d1: 0.1000000000000000055511151231257827021181583404541015625
d2: 0.1
d3: 0.1
第一种方法通过构造函数传入 double
类型的参数并不能精确地获取到值,若想正确的创建 BigDecimal
,要么将 double
转换为字符串然后调用构造方法,要么直接调用静态方法。事实上,静态