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

前言

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

java处理高精度的商业计算

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


情形

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

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

需求是这样的:股份单位是 万股。比例是百分之多少(%); 
所以对于股份我们需要除以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);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

首先我们是传入两个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)
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这个和除法类似,首先把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);
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5

这里我要讲的是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);
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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

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

参数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); 
 
 
  • 1
  • 2
  • 3
  • 4


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

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)
 
 
  • 1
  • 2
  • 3


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

9.8599999999999994315658113919198513031005859375
0.40000000000000002220446049250313080847263336181640625
 
 
  • 1
  • 2


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

BigDecimal d1 = new BigDecimal("9.86");  
BigDecimal d2 = new BigDecimal("0.4");  
BigDecimal d3 = d1.divide(d2);  
System.out.println(d3); 
 
 
  • 1
  • 2
  • 3
  • 4


为什么可以这样呢?接下来,我们探索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
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16


通过这三个构造函数,可以把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
  • 2
  • 3
  • 4


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

《BigDecimal 的那些坑事儿》> http://blog.youkuaiyun.com/ugg/article/details/8213666

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值