1. 概述
对于金额的存储说起来实际有很多问题,比如能不能用浮点类型做金额的计算,在应不应该把计算好的最终金额结果直接set到数据库等等
2. 浮点类型
2.1 float
vs double
浮点类型用于表示有小数部分的数值,在java中有两种浮点类型
类型 | 存储要求 | 取值范围 |
---|---|---|
float | 4字节 | 有效位数为6-7位 |
double | 8字节 | 有效位数为15位 |
double
表示这种类型的数值精度是float
类型的两倍;绝大部分应用程序都采用double类型,很多情况下,float
类型的精度很难满足需求
float
类型的数值后面有一个后缀F或f,例如3.14f
,没有后缀 F 的浮点数值(如 3.14
) 默认为double
类型。当然,也可以在浮点数值后面添加后缀 D 或 d (3.14D
)
2.2 浮点数值不适用于金融计算
为什么浮点数值不适用于金融计算
浮点数值不适用于无法接受舍入误差的金融计算中,float和double主要是为了科学计算和工程计算而设计的,他们执行二进制浮点运算, 这是为了在广泛的数值范围上提供较为精确的快速近似计算而精心设计的,但是他们无法提供完全精准的结果,因为二进制系统中无法精确地表示分数 1/10
,这 就好像十进制无法精确地表示分数 1/3
—样;
例如,命令 System.out.println ( 2.0-1.1 )
将打印出 0.8999999999999999
, 而不是人们想象的 0.9
3. 如何在程序中进行金额的准确计算
有三种方法解决上面浮点数计算不准确的问题,使用int
,long
或BigDecimal
来进行货币的计算
3.1 使用BigDecimal
BigDecimal基本使用
但是使用BigDecimal
很不方便而且运算很慢,同时我们要注意,在使用BigDecimal的时候最好是使用字符串构造函数
构造函数
关于BigDecimal(Double.toString(double val))
与BigDecimal(double val)
的区别
BigDecimal(String val)
//将会把String型转换成BigDecimal.
BigDecimal(double val)
//将会把double型二进制浮点型值精确的转换成十进制的BigDecimal.在BigDecimal(double val)中有如下的说明:
- 这个构造函数的结果有点难以预测。你可能认为java中用
new BigDecimal(0.1)
创建的BigDecimal
应该等于0.1
(一个是1的无精度的值,一个是有精度的值),但实际上精确的是等于0.1000000000000000055511151231257827021181583404541015625
。这是因为0.1
不能被double
精确的表示。因此,传入构造函数的值不是精确的等于0.1
。 - 而
String
参数的构造函数就能够得到很好的预测:如同我们认为的那样,new BigDecimal("0.1")
完全等于0.1
.因此,建议优先使用BigDecimal(String val)
构造函数。 - 如果必须将
double
型传入BigDecimal
,要注意该构造函数是一个精确的转换,它无法得到与先调用Double.toString(double)
方法将double
转换成String
,再使用BigDecimal(String)
构造函数一样的结果
基本方法
//返回这个大实数与另一个大实数 other 的和、 差、 积、 商
BigDecimal add(BigDecimal other)
BigDecimal subtract(BigDecimal other)
BigDecimal multipiy(BigDecimal other)
//要想计算商, 必须给出舍入方式 (rounding mode)。
//RoundingMode.HALF UP 是四舍五入方式。它适用于常规的计算
BigDecimal divide(BigDecimal other RoundingMode mode)
//如果这个大实数与另一个大实数相等, 返回 0 ; 如果这个大实数小于另一个大实数, 返回负数; 否则,返回正数。
int compareTo(BigDecimal other)
static BigDecimal valueOf(long x)
//返回值为 X 或 x / 10^scale^ 的一个大实数
static BigDecimal valueOf(long x,int scale)
3.2使用int
或long
除了使用BigDecimal
之外还有一种方法就是使用int或者long,到底选择int还是long取决于所涉及数值的大小,同时自己处理十进制小数点
具体做法就是将元表示的金额转换为分用整形表示进行计算
所以说如果性能非常关键,同时又不介意自己处理小数点,那么可以使用整形,但是如果为了方便地控制舍人,使用BigDecimal
提供地方法是更好地选择
4. 金额计算是放在内存中进行还是数据库中进行
这是在知乎上看到的一个问题java计算金额是放在内存中计算还是在sql中计算好呢?
最近遇到了一个问题,就是上一任开发代表要求我们计算金额的时候放在sql中比如 update 语句中 直接set 金额-金额。而现在这一任开发代表要求我们在代码中计算,然后再update 金额。我想问一下这两种方法那种是正确的,各有什么优势和不足
首先光看问题本身,应该放在代码中写,为了更好的扩展性,低耦合性,业务代码应该在业务的service层去实现,而不是让dao层去实现;
但是数据库上的金额这个字段应不应该被直接set
?
严格来说,数据库上的金额这个字段根本就不应该被直接set,因为在这个操作完成以后,实际上数据库要承载的信息被丢失了,丢失了修改前的数值,而且是永久丢失了,再也找不回来了,这里就产生了一个信息上的黑洞,信息永远丢失了。
更加科学的设计,是设计一张基于有效时间的拉链表,每次金额变化的时候,写入一条新纪录,并将产生这次金额变化的原因记录下来备查。
如果再扩展一下,将金额变化的增加和减少分别纪录到两个字段,就构成会计学上的借贷记账法,如果再加上科目记账体系,就是复式记账法。复式记账法的优点在于记账过程是可逆的,而且可以自动进行稽核处理,确保记账过程是准确的,这个准确性是通过会计学的理论来保证和证明的。总的来说,涉及金额计算的时候,最理想的是使用复式记账法来记账处理,次之采用借贷记账来处理,再次之采用单边流水账拉链表来处理,最差的就是直接使用sql语句update 金额