Java精准计算

2018-02-27 11:45:35来源:https://www.jianshu.com/p/e3b46aa1fcd7作者:iStory人点击

分享


之前遇到需要对结果集做精准运算的需求,在java中,double和folat是无法进行精准运算的,因为double和float只能做科学计算,在商业计算中,无法满足,根本原因是计算机无法用精准的二进制保存十进制。后来找了下java中的精准运算类型,就是BigDecimal,那是不是知道了这个类,就能做精准计算的业务了呢,这是很多人的误区,因为很多细小的地方需要自己非常清楚的了解,才能写出不出错并且优雅的代码,总结了几个BigDeciaml的使用误区。


首先double和float只保存了相似值而非精确值,理解这点很重要,我们可以试下计算机中保存的值和实际的值之间的差异。


public static void testDouble() {
double d1 = 0.05;
double d2 = 0.01;
double result = d1 + d2;
System.out.println(result); //0.060000000000000005
}
public static void testFloat() {
float f1 = 0.05f;
float f2 = 0.01f;
float result = f1 + f2;
System.out.println(result); //0.060000002
}

最后的计算值并不是想象中的0.6而是保存了一组相似值,如果不理解这点,很容易在业务中计算错误,导致系统bug。


既然double和float都无法保存精确值,那么用什么来保存精确值尼?就是上面说的BigDecimal。BigDecimal初始化有这么几个重载,看几个例子。


   public static void testBigDecimalOfDouble() {
System.out.println("double");
double d1 = 0.06;
BigDecimal b = new BigDecimal(d1);
System.out.println(b); //0.059999999999999997779553950749686919152736663818359375
}
public static void testBigDecimalOfFloat() {
System.out.println("float");
float f1 = 0.06f;
BigDecimal b = new BigDecimal(f1);
System.out.println(b); //0.0599999986588954925537109375
}

初始化的时候我们可以看到如果用double和float初始化,同样无法得到精准值,这是由于传入的值本身的原因,因为double或者float本身就用二进制保存了相似值。所以如果想让BigDecimal得到精准值,就得在传入的时候,就得是精准值,有两个方法可以使得BigDecimal保存为精准值,一个是对传入的参数tostring,还有一个就是调用BigDecimal的valueof方法。


   public static void  testBigDecimalOfString(){
System.out.println("string");
double d=0.06;
float f=0.06f;
BigDecimal b=new BigDecimal("0.06");
BigDecimal b2=BigDecimal.valueOf(d);
BigDecimal b3=BigDecimal.valueOf(f);
BigDecimal b4=new BigDecimal(String.valueOf(f));
System.out.println(b); //0.06
System.out.println(b2); //0.06
System.out.println(b3); //0.05999999865889549
System.out.println(b4); //0.06
}

valueof的内部实现其实就是调用了Double的tostring方法,以下就是valueof的源码实现:


   public static BigDecimal valueOf(double val) {
// Reminder: a zero double returns '0.0', so we cannot fastpath
// to use the constant ZERO. This might be important enough to
// justify a factory approach, a cache, or a few private
// constants, later.
return new BigDecimal(Double.toString(val));
}

上面有一个地方需要注意,就是需要注意到b3这个值,为什么是一个相似值,而不是一个精准值,不是说tostring后就是精准值嘛?这是由于valueof内部调用了Double的tostring方法,double属于双精度,而float属于单精度,这个地方特别容易造成错误,在开发的时候,如果不是非常了解这种细节的时候。一般情况下用valueof方法即可,如果用new BigDecimal()方法,传入double或者float的时候,需要tostring。


在BigDecimal中还有3个final变量分别是ONE,ZERO,TEN,这三个final变量有什么用呢,可以直接翻看源代码,来窥探设计者的初衷。


 public static final BigDecimal ZERO =
zeroThroughTen[0];
/**
* The value 1, with a scale of 0.
*
* @since 1.5
*/
public static final BigDecimal ONE =
zeroThroughTen[1];
/**
* The value 10, with a scale of 0.
*
* @since 1.5
*/
public static final BigDecimal TEN =
zeroThroughTen[10];

源代码中可以看到这里访问了一个数组,好像这里并看不出什么端倪,直接继续转到定义,翻看源代码:


  private static final BigDecimal zeroThroughTen[] = {
new BigDecimal(BigInteger.ZERO, 0, 0, 1),
new BigDecimal(BigInteger.ONE, 1, 0, 1),
new BigDecimal(BigInteger.valueOf(2), 2, 0, 1),
new BigDecimal(BigInteger.valueOf(3), 3, 0, 1),
new BigDecimal(BigInteger.valueOf(4), 4, 0, 1),
new BigDecimal(BigInteger.valueOf(5), 5, 0, 1),
new BigDecimal(BigInteger.valueOf(6), 6, 0, 1),
new BigDecimal(BigInteger.valueOf(7), 7, 0, 1),
new BigDecimal(BigInteger.valueOf(8), 8, 0, 1),
new BigDecimal(BigInteger.valueOf(9), 9, 0, 1),
new BigDecimal(BigInteger.TEN, 10, 0, 2),
};

转到数组,我们就可以看到为什么这么设计了,一个static数组,作者用static全局变量来做缓存,保存了0~10这11个变量,不过有一点我想不明白为什么把这11个数字做缓存,而不是其他数字,难道这几个数字使用频率最高嘛,如果是频率最高的,那又是如何统计出来的呢?可是我觉得这几个数字的频率应该并不是最高的,商业计算大多都是大额数字,这点让我想不明白。


了解了内部的缓存机制,我们在写代码的时候,如果返回这11个数字中的其中一个,就可以像下面这样写,直接返回缓存。


   public static BigDecimal zeroThroughTen() {
try {
BigDecimal result = new BigDecimal("a");
return result;
} catch (Exception e) {
return BigDecimal.ZERO;
//return BigDecimal.valueOf(0);
}
}

使用valueof的效果是一样的,valueof内部会自动计算传入的数字是否在zeroThroughTen这个数组里面,如果在里面,直接返回缓存中的值,源代码的判断如下:


 public static BigDecimal valueOf(long val) {
if (val >= 0 && val < zeroThroughTen.length)
return zeroThroughTen[(int)val];
else if (val != INFLATED)
return new BigDecimal(null, val, 0, 0);
return new BigDecimal(INFLATED_BIGINT, val, 0, 0);
}

那为什么要使用缓存嘞,缓存无非就是想让应用程序更快,不会产生额外的开销,因为BigDecimal是引用类型,并且是不可变类型,这就意味着,每次都会创建对象,如果在大量运算当中,就会产生大量的创建,销毁对象,所以用了缓存。java中有很多类型调用对应的方法的时候,是直接对地址修改的,这和我之前在c#中调用的方法不太一样的。但是BigDecimal是属于高精度计算的,必须保证对象不可变,如果变了那就出问题了,试想计算金额的时候,这个对象轻易改变,那么钱就错了。


BigDecimal的比较,有对象就有比较,比较方法在引用类型中见的最多的就是equals和compare方法,BigDecimal的比较方法,我第一次使用的时候,下巴掉到了地上,因为之前用过c#的比较方法,c#中decimal是有值类型的,并且重写了运算符比较,用着非常方便,所以用BigDecimal的比较的时候,非常不习惯,后来一想,c#是后来居上,避免了java中很多设计问题,这也不足为奇了。来看下BigDecimal的比较方法。


 public static void testEquals() {
System.out.println("equals");
BigDecimal decimal1 = new BigDecimal(1.2);
BigDecimal decimal2 = new BigDecimal(1.20);
BigDecimal str1 = new BigDecimal("1.2");
BigDecimal str2 = new BigDecimal("1.20");
System.out.println(str1.equals(str2)); //false
System.out.println(decimal1.equals(decimal2)); //true
System.out.println(decimal1.equals(str2)); //false
}
public static void testCompare() {
System.out.println("compare");
BigDecimal decimal1 = new BigDecimal(1.2);
BigDecimal decimal2 = new BigDecimal(1.20);
BigDecimal str1 = new BigDecimal("1.2");
BigDecimal str2 = new BigDecimal("1.20");
System.out.println(decimal1.compareTo(decimal2)); //0
System.out.println(str1.compareTo(str2)); //0
System.out.println(decimal1.compareTo(decimal2)); //0
System.out.println(decimal1.compareTo(str2)); //-1
}

BigDecimal中比较方法主要有两个,equals和compareTo,前者主要比较对象,只有在value和scale相等的时候,两个对象才相等,在equals方法中,如果传入的是string参数,1.2和1.20这两个值是不想等的,因为scale不等,内部并不会对scale做处理。可以看下testEquals方法中decimal1和decimal2的比较结果,姨,这里为什么相等了嘞,因为equals方法比较的是value和scale这里虽然传入的是1.2和1.20,还记得之前讲过的嘛,如果传入double值,BigDecimal将得到double本身的相似值,而非精准值,decimal1和decimal2其实值是一样的,结果为:1.1999999999999999555910790149937383830547332763671875,这样value和scale完全一样,所以就为true,再来看下equals源码中对于equals的描述,记住这个注释,就弄清楚了equals的原理:


/**
* Compares this {@code BigDecimal} with the specified
* {@code Object} for equality. Unlike {@link
* #compareTo(BigDecimal) compareTo}, this method considers two
* {@code BigDecimal} objects equal only if they are equal in
* value and scale (thus 2.0 is not equal to 2.00 when compared by
* this method).
*/

所以在BigDecimal中比较用compareTo不能用equals,方法其实compareTo相当于< 和 > 和 = 这三个符号,只不过java没进行运算符重写,这也是逆天的设计。


   public static void testCompare() {
System.out.println("compare");
BigDecimal str1 = new BigDecimal("1.2");
BigDecimal str2 = new BigDecimal("1.20");
BigDecimal str3=new BigDecimal("2");
BigDecimal str4=new BigDecimal("0.9");

System.out.println(str1.compareTo(str2)); //0
System.out.println(str1.compareTo(str3)); //-1
System.out.println(str1.compareTo(str4)); //1
}

BigDecimal运算,在进行BigDecimal计算的时候,我的下巴再次掉到了地上,因为+-*/ 这四个运算符也没重写,这得多别扭,每次运算的时候,我得调用方法,才能进行运算,需要注意的是,再进行除法运算的时候,如果遇到除不尽的情况,如果不指定scale参数,将会抛出异常。


public static void operation() {
System.out.println("operation");
BigDecimal a1 = BigDecimal.valueOf(10);
BigDecimal a2 = BigDecimal.valueOf(3);
System.out.println(a1.add(a2)); //13
System.out.println(a1.subtract(a2)); //7
System.out.println(a1.multiply(a2)); //30
System.out.println(a1.divide(a2,BigDecimal.ROUND_FLOOR,2)); //3.334
}

BigDecimal舍入模式。这个是我最难弄明白的,因为注释写的太简单了,其中有几个不同的模式,对于简单的数字,结果可能都是一样的。所以这个舍入模式得非常细的弄清楚每个枚举的区别。比如up这个枚举,意思是向远离0的方向舍入,这啥意思啊,乍一看每个字都认识,咋拼到一起就不懂了尼。这句话啥意思嘞,意思就是说:始终对非零舍弃部分前面的数字加 1,就是说up是不做四舍五入的,不管后面的是小于5还是大于5,都向前面进1.


public static void testUp() {
System.out.println("up");
BigDecimal temp = BigDecimal.valueOf(1.61);
BigDecimal t1 = temp.setScale(1, BigDecimal.ROUND_UP);
System.out.println(t1); //1.7
BigDecimal temp2=BigDecimal.valueOf(1.67);
t1=temp2.setScale(1,BigDecimal.ROUND_UP);
System.out.println(t1); //1.7
BigDecimal temp3=BigDecimal.valueOf(-1.61);
t1=temp3.setScale(1, BigDecimal.ROUND_UP);
System.out.println(t1); //-1.7
}

DOWN就是up的相反逻辑,意思就是:向零方向舍入:


public static  void  testDown(){
System.out.println("down");
BigDecimal temp = BigDecimal.valueOf(1.61);
BigDecimal t1 = temp.setScale(1, BigDecimal.ROUND_DOWN);
System.out.println(t1); //1.6
BigDecimal temp2=BigDecimal.valueOf(1.67);
t1=temp2.setScale(1,BigDecimal.ROUND_DOWN);
System.out.println(t1); //1.6
BigDecimal temp3=BigDecimal.valueOf(-1.61);
t1=temp3.setScale(1, BigDecimal.ROUND_DOWN);
System.out.println(t1); //-1.6
}

CEILING的逻辑就是正数的时候走up,负数的时候走down.


FLOOR的逻辑与CEILING有点相反,正数走down,负数走up。


HALF_UP的逻辑最好理解了,就是小学就会的四舍五入,基本上财务运算很少会运用四舍五入,因为设计不科学,所以四舍五入的算法一般在业务中很少会采用,一般采用的是银行家舍入。


HALF_DOWN的逻辑就是五舍六入。


HALF_EVEN应用场景主要用于银行,普通公司的业务一般用不到这么精确(一般公司的处理逻辑是这样的,金额精确到两位,只要超出两位,不管末尾多少,直接舍弃,简单粗暴),既银行家舍入法,就是通常说的四舍六入五取偶,这个也是大部分编程语言采用的算法,具体逻辑是这样的:如果舍弃部分左边的数字为奇数,则舍入行为同 RoundingMode.HALF_UP;如果为偶数,则舍入行为同RoundingMode.HALF_DOWN。


 public  static  void  testHalfEven(){
System.out.println("halfeven");
BigDecimal b=BigDecimal.valueOf(1.736);
BigDecimal t1=b.setScale(2,BigDecimal.ROUND_HALF_EVEN);
System.out.println(t1); //1.74
BigDecimal b2=BigDecimal.valueOf(1.745);
t1=b2.setScale(2,BigDecimal.ROUND_HALF_EVEN);
System.out.println(t1); //1.74
}

NNECESSARY主要用于断言,平时用的也相对较少,用于断言精确的小数点,否则抛出异常,以下代码用于精确断言一位小数点,下面代码将抛出异常:


 public  static  void  testNNECESSARY(){
System.out.println("NNECESSARY");
BigDecimal b=BigDecimal.valueOf(1.78);
BigDecimal t1=b.setScale(1,BigDecimal.ROUND_UNNECESSARY);
System.out.println(t1);
}

了解了BigDecimal的这些特性,那么设计一般的金融业务和平常公司中的支付,充值,提现,金额流水这些业务基本没问题了,BigDecimal这些特性,有很多地方还是有很多注意点的,所以掌握基础才是关键,如果不知道这些细节的地方,很可能在某一个细节处,出现bug,可能有些地方自己写的时候是对的,qa也没发现,一旦到了线上各种数据来了,就非常有可能出现bug,所以只有对这些细节的了解和把握,才能写出不容易出错的代码和逻辑,很多人不屑于这些基础细节,可能只知道金额计算要用到这个类,对于细节的方法和jdk内部的设计不了解,这样就容易造成错误和损失。大的系统和设计离不开底层的这些细节,细节不掌握何谈框架和系统设计。








最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台