java使用BigDecimal 处理商业精度及高精度详解

java使用BigDecimal 处理商业精度及高精度详解
2016年11月15日 19:47:54
阅读数:2645
前言

之前我是写过一篇类似笔记:

java处理高精度的商业计算

但是呢,写的太简单,关键还没有写到要点,所以重新写一篇。
情形

由于公司最近要求把股票相关的数据,全部交给后端来处理,不再由前端来处理。
股票大家都知道,这里面的计算都是商业级别的,小数点4+位那是再正常不过啦。
比如这样几组数字

2539230979.0000 //流通受限股份
8680253870 //某个股东持股数
0.4081 //某某股东所占总股数的比例

 

需求是这样的:股份单位是 万股。比例是百分之多少(%);
所以对于股份我们需要除以10000,保留2位小数
对于比例 是要乘以100,保留2位小数。
除法

首先我们来写除法。

/**
 * scale 小数点保留几位
 */
public static BigDecimal divi(double v1,double v2, int scale){
    BigDecimal b1 = new BigDecimal(String.valueOf(v1));
    BigDecimal b2 = new BigDecimal(String.valueOf(v2));
    return b1.divide(b2, scale, BigDecimal.ROUND_HALF_UP);
}



首先我们是传入两个double类型的参数和精度(小数点保留的位数),
我们再先转为String类型后,在利用BigDecimal的构造方法来生成BigDecimal对象v1、v2。
v1.divide(v2…)就是v1除以v2,保留scale为小数,BigDecimal.ROUND_HALF_UP就是我们学的四舍五入。
乘法

public static BigDecimal muli(double v1, double v2, int scale){
    BigDecimal b1 = new BigDecimal(String.valueOf(v1));
    BigDecimal b2 = new BigDecimal(String.valueOf(v2));
    BigDecimal multiply = b1.multiply(b2);
    return multiply.setScale(scale, BigDecimal.ROUND_HALF_UP)
}



这个和除法类似,首先把v1 、v2转成BigDecimal对象,然后调用BigDecimal中的multiply方法,
这个方法不像divide可以设置精度,所以得使用setScale()方法来设置精度。
设置精度(保留几位小数)

public static BigDecimal scale(double v1, int scale){
    //String.valueOf(v1)
    BigDecimal b1 = new BigDecimal(Double.toString(v1));
    return b1.setScale(scale, BigDecimal.ROUND_HALF_UP);
}



这里我要讲的是Double.toString(v1)方法是把v1转成字符串。String.valueOf(v1),也是一样的。
那两者的区别是什么呢?其实String.valueOf(v1)源码里就是调用Double.toString(v1)的方法。
上面这种设置精度方法,有个问题:
要是double v1 = 0.0002,执行scale(v1, 2)时,得到的答案:0.00,其实有时候我们是想保存有效数字。
设置有效精度(保留有效位数)

    /**
     * 保留有效位(eg:0.00002 -- 得到的是0.000020)
     *
     * @author yutao
     * @return
     * @date 2016年11月14日下午1:27:28
     */
    public static BigDecimal validScale(double v1, int scale){
        if (scale < 0) {  
             throw new IllegalArgumentException("The scale must be a positive integer or zero");  
         }
        BigDecimal b = new BigDecimal(String.valueOf(v1));  
        BigDecimal divisor = BigDecimal.ONE;  
        MathContext mc = new MathContext(scale);
        return b.divide(divisor, mc);
    }

 

这里就用到了MathContext类,它的构造方法有:

1、MathContext(int setPrecision)
2、MathContext(int setPrecision, RoundingMode setRoundingMode)
3、MathContext(String val)


参数setPrecision是指有效位数,不是指保留小数多少位。之前就在这里坑到过。
参数setRoundingMode是指舍入模式。这个和BigDecimal类似。
也就是说要想设置有效位,就是通过MathContext来设置的。
一般常用第1、第2中构造方法

    枚举常量摘要
    ROUND_CEILING
    向正无限大方向舍入的舍入模式。
    ROUND_DOWN
    向零方向舍入的舍入模式。
    ROUND_FLOOR
    向负无限大方向舍入的舍入模式。
    ROUND_HALF_DOWN
    向最接近数字方向舍入的舍入模式,如果与两个相邻数字的距离相等,则向下舍入。
    ROUND_HALF_EVEN
    向最接近数字方向舍入的舍入模式,如果与两个相邻数字的距离相等,则向相邻的偶数舍入。
    ROUND_HALF_UP
    向最接近数字方向舍入的舍入模式,如果与两个相邻数字的距离相等,则向上舍入。
    ROUND_UNNECESSARY
    用于断言请求的操作具有精确结果的舍入模式,因此不需要舍入。(默认模式)
    ROUND_UP
    远离零方向舍入的舍入模式。

BigDecimal构造方法应使用String类型的
例子


假设我们先使用Double类型的构造方法。

BigDecimal d1 = new BigDecimal(9.86);  
BigDecimal d2 = new BigDecimal(0.4);  
BigDecimal d3 = d1.divide(d2);  
System.out.println(d3);


我们这样执行后,会报如下异常:

Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
    at java.math.BigDecimal.divide(BigDecimal.java:1616)
    at common.ToolsUtil.main(ToolsUtil.java:164)


原因:创建BigDecimal时,0.6和0.4是浮动类型的,浮点型放入BigDecimal内,其存储值为

9.8599999999999994315658113919198513031005859375
0.40000000000000002220446049250313080847263336181640625




这两个浮点数相除时,由于除不尽,而又没有设置精度和保留小数点位数,导致抛出异常。
但是要是我们使用String构造方法就OK

BigDecimal d1 = new BigDecimal("9.86");  
BigDecimal d2 = new BigDecimal("0.4");  
BigDecimal d3 = d1.divide(d2);  
System.out.println(d3);



为什么可以这样呢?接下来,我们探索BigDecimal原理:
BigDecimal,不可变的、任意精度的有符号十进制数。
BigDecimal 由任意精度的整数非标度值 和 32 位的整数标度(scale) 组成。
如果为零或正数,则标度是小数点后的位数。
如果为负数,则将该数的非标度值乘以 10 的负 scale 次幂。
因此,BigDecimal 表示的数值是 (unscaledValue × 10-scale)。我们知道BigDecimal有三个主要的构造函数

1
public BigDecimal(double val)

将double表示形式转换为BigDecimal

2

public BigDecimal(int val)

将int表示形式转换为BigDecimal

3

public BigDecimal(String val)

将字符串表示形式转换为BigDecimal




通过这三个构造函数,可以把double类型,int类型,String类型构造为BigDecimal对象,
在BigDecimal对象内通过BigIntegerintVal存储传递对象数字部分,通过int scale;记录小数点位数,
通过int precision;记录有效位数(默认为0)。

BigDecimal的加减乘除就成了BigInteger与BigInteger之间的加减乘除,浮点数的计算也转化为整形的计算,
可以大大提供性能,并且通过BigInteger可以保存大数字,从而实现真正大十进制的计算,
在整个计算过程中,还涉及scale的判断和precision判断从而确定最终输出结果。

通过上面的例子可以看出String的构造函数就是通过BigInteger记录BigDecimal的值,
使其计算变成BigInteger之间的计算。所以我们一般最好使用String类型的构造方法。


那如果非要使用Double类型的构造方法呢?
我们可以利用divide设置精度的方式来做

BigDecimal d1 = new BigDecimal(9.86);  
BigDecimal d2 = new BigDecimal(0.4);  
BigDecimal d3 = d1.divide(d2 ,1 , BigDecimal.ROUND_HALF_UP);  
System.out.println(d3);

通过/1,然后设置保留小数点方式,以及设置数字保留模式,从而得到两个数乘积的小数部分。
也就是给它设置好精度和舍入模式,就OK啦。(它就是通过舍入方式得到正确的答案)

《BigDecimal 的那些坑事儿》> http://blog.csdn.net/ugg/article/details/8213666
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013066244/article/details/53172243
文章标签: java BigDecimal
个人分类: Java


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值